Laravel 2FA with Nuxt 3
Hi everyone,
I am working on implementing Two-Factor Authentication (2FA) in my Laravel 10 backend API, which will be consumed by my Nuxt 3 application. Here’s a breakdown of what I’ve done so far, and I’d appreciate any insights or suggestions to why its not working
in the console window i get the user ,
{
"data": {
"userID": 2,
"email": "[email protected]",
"email_verified_at": null,
"last_ip_address": "teh ip address",
"last_device_details": "the device details",
"created_at": "2025-01-24T09:34:01.000000Z",
"updated_at": "2025-01-24T11:01:26.000000Z",
"google2fa_secret": "OVVLQ5OM2G33GER3",
adn in the https://urlpath/nova/api/enable-2fa i get
{
"qr_code_url": "otpauth:\/\/totp\/Nova:admin%40level-7.co.za?secret=OLQETEPKVQ52SSFO&issuer=Nova&algorithm=SHA1&digits=6&period=30",
"secret": "OLQETEPKVQ52SSFO"
}
but i get this errors
chunk-QF35QLYF.js?v=20aa8711:7424 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'type')
{data: RefImpl, pending: RefImpl, error: ObjectRefImpl, status: RefImpl, execute: ƒ, …}
No QR code URL in response
enable2FA @ enable2FA.js:26
await in enable2FA
(anonymous) @ TwoFactorAuth.vue:48
Promise.then
_createVNode.onUpdate:modelValue._cache.<computed>._cache.<computed> @ [userID].vue:109
TwoFactorAuth.vue:52 Failed to fetch QR Code URL
- Setting up Laravel Packages for 2FA: I started by installing the necessary packages to handle 2FA:
Google2FA: I used the pragmarx/google2fa-laravel package to handle the generation and validation of OTP codes. Bacon QR Code: This package, bacon/bacon-qr-code, is used to generate QR codes for the user to scan in their Google Authenticator app.
composer require pragmarx/google2fa-laravel
composer require bacon/bacon-qr-code
in my UserController Methods in Laravel: Next, I implemented the 2FA functionality in my UserController. Here are the two key methods I created:
here I im Enabling 2FA (enable2FA): This method generates a secret key for the user, stores it in the database, and then generates a QR code URL for the user to scan. I use the Google2FA service to generate the secret and QR code URL, then return the QR code URL to the frontend.
public function enable2FA(Request $request)
{
$google2fa = new Google2FA();
$secret = $google2fa->generateSecretKey();
$user = auth()->user();
$user->google2fa_secret = $secret;
$user->save();
$qrCodeUrl = $google2fa->getQRCodeUrl(
'Nova', // Your app's name
$user->email, // Use email or username for identification
$secret
);
return response()->json([
'qr_code_url' => $qrCodeUrl,
'secret' => $secret,
]);
}
Verifying 2FA (verify2FA): This method verifies the OTP submitted by the user. The OTP is checked using the verifyKey method from the Google2FA package. If valid, a success message is will the be returned
public function verify2FA(Request $request)
{
$request->validate([
'otp' => 'required|digits:6', // Ensure OTP is a 6-digit number
]);
$user = auth()->user();
$google2fa = new Google2FA();
$valid = $google2fa->verifyKey($user->google2fa_secret, $request->otp);
if ($valid) {
return response()->json(['message' => '2FA verified successfully']);
} else {
return response()->json(['message' => 'Invalid OTP'], 400);
}
}
In the Database Migration for Storing the 2FA Secret: I added a google2fa_secret column to the users table via a migration. This will store the user’s secret key.
php artisan make:migration add_google2fa_secret_to_users_table --table=users
Migration:
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('google2fa_secret')->nullable();
});
}
Setting up Routes for API Access: I’m using Laravel Sanctum for API authentication, I set up routes for enabling and verifying 2FA that are protected by Sanctum's authentication middleware.
Route::middleware(['auth:sanctum'])->group(function () {
Route::post('/enable-2fa', [UserController::class, 'enable2FA']);
Route::post('/verify-2fa', [UserController::class, 'verify2FA']);
});
Integrating 2FA with Nuxt 3 Frontend: On the frontend, I created a Nuxt composable to handle enabling 2FA and verifying the OTP. The composable communicates with the Laravel backend to retrieve the QR code URL and send the OTP for verification.
a) Composable for Enabling 2FA (useEnable2FA.js): In this composable, I use useApiFetch to call the /enable-2fa endpoint to retrieve the QR code URL. This URL is then displayed on the frontend.
export const useEnable2FA = () => {
const qrCodeUrl = ref(null);
const status = ref(null);
const error = ref(null);
const enable2FA = async () => {
try {
const response = await useApiFetch('/api/enable-2fa', {
method: 'POST',
body: { someData: 'value' },
});
const data = response?.data?.value || response?.value;
if (data && data.qrCodeUrl) {
qrCodeUrl.value = data.qrCodeUrl;
} else {
console.error('No QR code URL in response');
}
} catch (err) {
console.error('Error enabling 2FA:', err);
}
};
return { qrCodeUrl, enable2FA, status, error };
};
Page Component for 2FA Setup (TwoFactorAuth.vue): This Vue component handles the user interface for enabling 2FA. It should displays the QR code and allows the user to input their OTP.
<script setup>
import { onBeforeMount, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useEnable2FA } from '~/composables/enable2FA'; // Import the composable
const { qrCodeUrl, enable2FA, status, error } = useEnable2FA(); // Use the composable
const otp = ref('');
const isOtpInserted = ref(false);
const isVerifyingOtp = ref(false); // Added state for verifying OTP
const router = useRouter();
// Verifying OTP
const verifyOtp = async () => {
try {
isVerifyingOtp.value = true; // Set verifying OTP to true
const response = await useApiFetch('/api/verify-2fa', {
method: 'POST',
body: { otp: otp.value },
});
if (response?.message) {
console.log(response.message);
router.push('/'); // Redirect or show success message
} else {
console.error('OTP verification failed');
}
} catch (error) {
console.error('Error verifying OTP:', error);
} finally {
isVerifyingOtp.value = false; // Reset after verifying OTP
}
};
// Handle OTP form submission
const onFinish = async () => {
isOtpInserted.value = true;
await verifyOtp(); // Call OTP verification
setTimeout(() => {
isOtpInserted.value = false;
}, 2000);
};
// Initialize 2FA when the component is mounted
onBeforeMount(async () => {
await enable2FA(); // This will trigger the 2FA setup
// Check if qrCodeUrl has been set successfully
if (!qrCodeUrl.value) {
console.error('Failed to fetch QR Code URL');
alert('Failed to load QR code. Please try again later.');
}
});
</script>
<template>
<div class="auth-wrapper">
<div class="auth-card">
<h4>Two Step Verification</h4>
<p>We sent a verification code to your mobile. Enter the code from the mobile in the field below.</p>
<div v-if="qrCodeUrl" class="qr-code-container">
<img :src="qrCodeUrl" alt="Scan this QR Code with Google Authenticator" />
</div>
<VForm @submit.prevent="onFinish">
<VRow>
<VCol cols="12">
<h6>Type your 6 digit security code</h6>
<VOtpInput v-model="otp" :disabled="isOtpInserted || isVerifyingOtp" />
</VCol>
<VCol cols="12">
<VBtn :loading="isVerifyingOtp" block type="submit">Verify my account</VBtn>
</VCol>
</VRow>
</VForm>
</div>
</div>
</template>
- Next Steps: The frontend will allow the user to scan the QR code and enter their OTP. The OTP will then be sent to the backend for verification, and the user will be redirected or shown a success message upon successful verification. This is where I am at right now, and I out of ideas to why its not working
Thanks!
Please or to participate in this conversation.