Laravel Vue.js Authentication with Google 2FA – Secure Login Step-by-Step Guide

Securing your user authentication system is essential for any modern web application. In this guide, we’ll show you how to implement a Laravel Vue.js authentication with Google 2FA system that adds an extra layer of protection using time-based one-time passwords (TOTP).

You’ll learn how to integrate Google Two-Factor Authentication (2FA) into both your Laravel backend and Vue.js frontend, ensuring that only verified users can log in — even if their password is compromised.

This tutorial also builds upon the foundation from our previous guide, Laravel 12 Vue 3 Session Based Authentication – Complete SPA Tutorial for Beginners, extending it with robust account protection using Google Authenticator for enhanced login security.

Step 1: Check Out the Previous Tutorial First

Before starting this guide, make sure you’ve already completed the setup from the previous post: Laravel 12 Vue 3 Session Based Authentication – Complete SPA Tutorial for Beginners.
👉 This tutorial builds on top of that project structure, so having it ready will help you continue smoothly.

You can read it here: https://laravelcenter.com/laravel-12-vue-3-session-based-authentication/

Step 2: Install Google 2FA Package

Begin your Laravel Vue.js authentication with Google 2FA setup by installing the required package to generate and verify time-based one-time passwords (TOTP).

We’ll use the official and well-maintained package pragmarx/google2fa to generate secret keys and verify codes.

Run this command in your Laravel project root:

composer require pragmarx/google2fa

This package handles all the core logic for generating and verifying time-based one-time passwords (TOTP).

Step 3: Add 2FA Fields to Users Table

Enhance your Laravel Vue.js authentication with Google 2FA by adding database fields to store each user’s secret key and enable or disable two-factor authentication.

Create a migration file:

php artisan make:migration add_two_factor_columns_to_users_table

Then open the generated migration and add:

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('google2fa_secret')->nullable();
        $table->boolean('is_2fa_enabled')->default(false);
    });
}

Run migration:

php artisan migrate

Step 4: Generate 2FA Secret and QR Code

In this step of Laravel Vue.js authentication with Google 2FA, you’ll create a unique secret for each user and generate a QR code for Google Authenticator setup.

Edit API controller file app/Http/Controllers/Api/AuthController.php, modify the login method:

// Laravel Vue.js authentication with Google 2FA
use PragmaRX\Google2FA\Google2FA;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;

// Login 
public function login(Request $request)
{
    // Verify 2FA
    if ($request->require_otp) {
        $rules = [
            'otp' => 'required'
        ];

        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails())
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ]);

        // Verify 2FA OTP code
        $google2fa = new Google2FA();
        $user = User::where('email', $request->email)->first();

        if ($user->google2fa_secret)
            $secret = $user->google2fa_secret;
        else
            $secret = $request->secret;

        if (!$google2fa->verifyKey($secret, $request->otp)) {
            return response()->json([
                'success' => false,
                'errors' => [
                    'otp' => ['Invalid 2FA code']
                ]
            ]);
        }

        // save secret key to user
        if (!$user->google2fa_secret) {
            $user->google2fa_secret = $secret;
            $user->save();
        }
    }
    // Login
    else {
        $rules = [
            'email' => 'required|email',
            'password' => 'required',
        ];

        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails())
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ]);

        $user = User::where('email', $request->email)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            return response()->json(
                [
                    'success' => false,
                    'errors' => [
                        'email' => ['Incorrect email or password']
                    ]
                ]
            );
        }

        // check 2FA
        if ($user->is_2fa_enabled) {
            $google2fa = new Google2FA();
            $qrCodeSvg = '';
            $secretKey = '';

            if (!$user->google2fa_secret) {
                // generate a secret key for this user
                $secretKey = $google2fa->generateSecretKey();
                // Create QR Code (SVG)
                $renderer = new ImageRenderer(
                    new RendererStyle(200),
                    new SvgImageBackEnd()
                );
                $writer = new Writer($renderer);

                $qrCodeUrl = $google2fa->getQRCodeUrl(
                    config('app.name'),
                    $user->email,
                    $secretKey
                );

                $qrCodeSvg = $writer->writeString($qrCodeUrl);
            }

            return response()->json(
                [
                    'success' => false,
                    'require_otp' => true,
                    'email' => $user->email,
                    'qrcode' => $qrCodeSvg,
                    'secret' => $secretKey
                ]
            );
        }
    }

    Auth::login($user);

    return response()->json([
        'success' => true,
        'user' => $user->only('id', 'name', 'email'),
    ]);
}

You can use bacon/bacon-qr-code to generate QR codes easily.

Install it:

composer require bacon/bacon-qr-code

Step 5: Display 2FA QR Code In Vue.js Login Component

Integrate the generated QR code into your Vue.js component so users can easily scan it during their Laravel Vue.js authentication with Google 2FA setup process.

resources/js/Pages/Login.vue

<template>
    <section class="vh-100">
        <div class="container py-5 h-100">
            <div class="row d-flex justify-content-center align-items-center h-100">
                <div class="col-12 col-md-8 col-lg-6 col-xl-5">
                    <div class="card shadow-2-strong" style="border-radius: 1rem; background: lightsteelblue;">
                        <div class="card-body p-5" v-if="!form.require_otp">
                            <h1 class="mb-4 text-center fw-bold">Sign In</h1>
                            <form @submit.prevent="login">
                                <div class="form-outline mb-4">
                                    <input type="text"
                                        :class="['form-control form-control-lg', { 'is-invalid': errors?.email }]"
                                        placeholder="Email" ref="autofocus" v-model="form.email"
                                        :disabled="processing" />
                                    <span v-if="errors?.email" class="text-danger form-label" style="padding-left: 5px">
                                        {{ errors?.email[0] }}
                                    </span>
                                </div>
                                <div class="form-outline mb-4">
                                    <input type="password"
                                        :class="['form-control form-control-lg', { 'is-invalid': errors?.password }]"
                                        placeholder="Password" v-model="form.password" :disabled="processing" />
                                    <span v-if="errors?.password" class="text-danger form-label"
                                        style="padding-left: 5px">
                                        {{ errors?.password[0] }}
                                    </span>
                                </div>
                                <div class="d-grid mb-2">
                                    <button class="btn btn-primary btn-lg" type="submit" :disabled="processing">
                                        <span v-show="processing" class="spinner-border spinner-border-sm" role="status"
                                            aria-hidden="true"></span>
                                        Login
                                    </button>
                                </div>
                            </form>
                            <div class="py-3 text-center fs-5">
                                Don't have an account? <RouterLink :to="{ path: '/register' }">Register</RouterLink>
                            </div>
                        </div>
                        <div class="card-body p-5" v-else>
                            <h3 class="mb-4 text-center fw-bold">Two-Factor Authentication</h3>
                            <div v-if="form.secret">
                                <p class="mb-0">Scan this QR Code in Google Authenticator</p>
                                <div v-html="qrcode_2fa"></div>
                                <p class="my-2">Or enter this secret manually: <b>{{ form.secret }}</b></p>
                            </div>
                            <form @submit.prevent="login">
                                <div class="form-outline mb-4">
                                    <input type="text" class="form-control form-control-lg"
                                        placeholder="Enter Google Authenticator code" v-model="form.otp"
                                        ref="autofocus" />
                                    <span v-if="errors?.otp" class="text-danger" style="padding-left: 5px">
                                        {{ errors?.otp[0] }}
                                    </span>
                                </div>
                                <div class="mb-2 text-center">
                                    <button class="btn btn-danger btn-lg mx-2" type="button" :disabled="processing"
                                        @click="remove2FALocalStorage">
                                        Cancel
                                    </button>
                                    <button class="btn btn-primary btn-lg mx-2" type="submit" :disabled="processing">
                                        Submit
                                    </button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
</template>

<script setup>
import { onMounted, ref } from "vue";
import { RouterLink, useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";

const qrcode_2fa = ref(localStorage.getItem("qrcode"));
const processing = ref(false);
const form = ref({
    email: localStorage.getItem("email"),
    secret: localStorage.getItem("secret"),
    require_otp: localStorage.getItem("require_otp"),
    password: null,
    otp: null,
});
const errors = ref(null);
const router = useRouter();
const autofocus = ref(null);

const login = async () => {
    processing.value = true;
    const auth = useAuthStore();
    const response = await auth.login(form.value);
    if (response.data.success) {
        remove2FALocalStorage();
        router.push('/');
    }
    else if (response.data.require_otp) {
        localStorage.setItem("require_otp", response.data.require_otp);
        localStorage.setItem("email", response.data.email);
        localStorage.setItem("qrcode", response.data.qrcode);
        localStorage.setItem("secret", response.data.secret);
        form.value.require_otp = response.data.require_otp;
        form.value.secret = response.data.secret;
        qrcode_2fa.value = response.data.qrcode;
        setTimeout(() => {
            autofocus.value.focus();
            autofocus.value.select();
        }, 5);
    }
    else {
        errors.value = response.data.errors;
        setTimeout(() => {
            autofocus.value.focus();
            autofocus.value.select();
        }, 5);
    }
    processing.value = false;
}

const remove2FALocalStorage = () => {
    localStorage.removeItem('require_otp');
    localStorage.removeItem('email');
    localStorage.removeItem('qrcode');
    localStorage.removeItem('secret');
    form.value.require_otp = "";
    form.value.secret = "";
    qrcode_2fa.value = "";
}

onMounted(() => {
    autofocus.value.focus();
});
</script>

Final Step: Running and Testing Your Project

To test your project, run the Laravel server:

php artisan serve

If your project also uses Vue.js, run the frontend in a separate terminal:

npm run dev

After both are running, open your browser and visit:

http://127.0.0.1:8000

Make sure all pages load and your features work as expected.

Verify that your Laravel Vue.js authentication with Google 2FA is working by scanning the QR code, entering the OTP, and confirming successful login protection.

  1. Log in to your app.
  2. Go to your 2FA setup page.
  3. Generate a QR code and scan it with Google Authenticator.
  4. Verify and enable 2FA.
  5. Logout and try logging in again — you’ll now be asked to enter a 2FA code.

Conclusion

By completing these steps, you’ve built a secure Laravel Vue.js authentication with Google 2FA system that protects user accounts with powerful two-factor verification.

Now your users can:

  • Scan QR codes using Google Authenticator
  • Generate time-based OTP codes
  • Verify 2FA during login

This feature significantly improves your application’s protection against unauthorized access and password leaks.

If you’re building a production-ready Laravel Vue.js app, adding Google 2FA authentication is one of the best ways to enhance security and user trust.

Senghok
Senghok

Senghok is a web developer who enjoys working with Laravel and Vue.js. He creates easy-to-follow tutorials and guides to help beginners learn step by step. His goal is to make learning web development simple and fun for everyone.

Articles: 44

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *