Search code examples
firebase-authenticationvuejs3laravel-8inertiajstwo-factor-authentication

How to implement 2FA in Laravel with Inertia.js and Vue 3?


I am working on a project using Laravel, Inertia.js, and Vue 3, and I want to implement two-factor authentication (2FA) for user login. My setup includes a MySQL database with a users table that contains a column 2fa_enabled to indicate whether 2FA is required for a user.

Here is the workflow I would like to achieve:

  1. When a user logs in for the first time, 2FA is required if 2fa_enabled is set to true.
  2. The system sends a verification code to the user's mobile number and email.
  3. The user enters the code, which is then verified.
  4. If the code is correct, the system saves the code in the database and redirects the user to the dashboard.
  5. If the code is incorrect, the user sees an error message.

I've encountered some issues during implementation, and I'm not sure how to properly set up the verification code generation and handling process. I've tried using Firebase for sending SMS codes and handling ReCaptcha verification, but I'm running into the following issues:

Uncaught (in promise) ReferenceError: Firebase is not defined Recaptcha verification failures
Handling code verification in my Vue 3 component

I would appreciate any guidance on how to:

  • Properly generate and handle 2FA codes using Firebase. Manage ReCaptcha verification.
  • Handle code verification in my Vue 3 component.

Here is how I'm setting up the ReCaptcha verifier and Firebase initialization:

import { initializeApp } from 'firebase/app';
import { getAuth, RecaptchaVerifier } from 'firebase/auth';

const firebaseConfig = {
    apiKey: "YOUR_API_KEY",
    authDomain: "YOUR_AUTH_DOMAIN",
    projectId: "YOUR_PROJECT_ID",
    storageBucket: "YOUR_STORAGE_BUCKET",
    messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
    appId: "YOUR_APP_ID",
    measurementId: "YOUR_MEASUREMENT_ID"
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

const recaptchaContainer = document.getElementById('recaptcha-container');
const recaptchaVerifier = new RecaptchaVerifier(recaptchaContainer, {
    size: 'invisible',
    callback: (response) => {
        console.log('reCAPTCHA resolved');
    },
    'expired-callback': () => {
        console.log('reCAPTCHA expired');
    }
}, auth);

async function getCode(phoneNumber) {
    try {
        const confirmationResult = await auth.signInWithPhoneNumber(phoneNumber, recaptchaVerifier);
        console.log('Code sent:', confirmationResult);
    } catch (error) {
        console.error('Error during sign-in with phone number:', error);
        console.error('Error code:', error.code);
    }
}

// Example usage
getCode('+91112223334');


Solution

  • i follow Firebase Official Documents, so i found some mistakes, now it's work fine.

    import { getAuth, RecaptchaVerifier, signInWithPhoneNumber} from "firebase/auth";
    
    initializeApp(firebaseConfig);
    const auth = getAuth();
    onMounted(() => {
    const recaptchaVerifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
    

    in this line

    <script setup>
    import { initializeApp } from 'firebase/app';
    import { getAuth, RecaptchaVerifier, signInWithPhoneNumber } from "firebase/auth";
    import { computed, ref, onMounted } from 'vue';
    import { router } from "@inertiajs/vue3";
    
    const props = defineProps({
        phoneNumber: {
            type: String,
            required: true,
        },
    });
    
    const verificationCode = ref('');
    let confirmationResult = null;
    
    const otpPart1 = ref('');
    const otpPart2 = ref('');
    const otpPart3 = ref('');
    const otpPart4 = ref('');
    const otpPart5 = ref('');
    const otpPart6 = ref('');
    
    const firebaseConfig = {
      apiKey: "****",
      authDomain: "****.firebaseapp.com",
      projectId: "****",
      storageBucket: "****.appspot.com",
      messagingSenderId: "****",
      appId: "5:****:web:****",
      measurementId: "****"
    };
    
    
    initializeApp(firebaseConfig);
    const auth = getAuth();
    
    onMounted(() => {
        const recaptchaVerifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
            size: 'invisible',
            callback: (response) => {
                console.log('reCAPTCHA resolved');
            },
            'expired-callback': () => {
                console.log('reCAPTCHA expired, please resolve reCAPTCHA again.');
            }
        }, auth);
        window.recaptchaVerifier = recaptchaVerifier;
        sendVerificationCode(props.phoneNumber);
    });
    
    function sendVerificationCode(phoneNumber) {
        const appVerifier = window.recaptchaVerifier;
    
        signInWithPhoneNumber(auth, phoneNumber, appVerifier)
            .then((result) => {
                confirmationResult = result;
            })
            .catch((error) => {
                console.error('Error sending verification code:', error);
            });
    }
    
    function handleFormSubmit() {
        const otpCode = `${otpPart1.value}${otpPart2.value}${otpPart3.value}${otpPart4.value}${otpPart5.value}${otpPart6.value}`;
        verifyCode(otpCode);
    }
    
    function verifyCode(verificationCode) {
        if (confirmationResult) {
            confirmationResult.confirm(verificationCode)
                .then((result) => {
                    router.post(route('admin.sendverificationcode'), { 'verificationCode': verificationCode })
    
                })
                .catch((error) => {
                    console.error('Error verifying code:', error);
                });
        } else {
            console.error('Confirmation result not available.');
        }
    }
    
    const maskedPhoneNumber = computed(() => {
        const phoneNumberLength = props.phoneNumber.length;
        const digitsToShow = 4;
        const startIndex = phoneNumberLength - digitsToShow;
        const maskedPart = '*'.repeat(startIndex);
        const unmaskedPart = props.phoneNumber.slice(startIndex);
        return maskedPart + unmaskedPart;
    });
    </script>
    <style scoped>
    .login-card .card-body {
        padding: 2.5rem !important;
    }
    
    .signIn-heading {
        margin-bottom: 20px;
    }
    
    .twoStepArea {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 15px;
    }
    
    .three-input-groups {
        display: flex;
    }
    
    .three-input-groups input[type="number"] {
        width: 36px;
        height: 49px;
        border-radius: 0;
        border: 1px solid #e2e2e2;
        outline: 0;
        padding: 10px 5px;
        text-align: center;
    }
    
    .three-input-groups input:first-child {
        border-top-left-radius: 6px;
        border-bottom-left-radius: 6px;
    }
    
    .three-input-groups input[type="number"]+input[type="number"] {
        border-left: 0;
    }
    
    .three-input-groups input:last-child {
        border-top-right-radius: 6px;
        border-bottom-right-radius: 6px;
    }
    
    .twoStep-spacer {
        font-size: 34px;
        font-weight: 700;
        color: #e2e2e2;
        line-height: 1;
        margin-top: -10px;
    }
    </style>
    <template>
    <div class="row justify-content-center">
                        <div class="col-md-8 col-lg-6 col-xl-5">
                            <div class="card mt-4 login-card">
                                <div class="card-body p-4">
                                    <div>
                                        <h5 class="text-primary signIn-heading">Two-step authentication</h5>
                                        <p>Enter the 6-digit verification code sent to <b>your mobile
                                                number {{ maskedPhoneNumber }}.</b></p>
                                    </div>
                                    <div>
                                        <form @submit.prevent="handleFormSubmit">
                                            <div id="recaptcha-container"></div>
                                            <div class="otp-container mt-4">
                                                <div class="twoStepArea">
                                                    <div class="three-input-groups">
                                                        <input type="number" v-model="otpPart1" maxlength="1"
                                                            class="otp-input" />
                                                        <input type="number" v-model="otpPart2" maxlength="1"
                                                            class="otp-input" />
                                                        <input type="number" v-model="otpPart3" maxlength="1"
                                                            class="otp-input" />
                                                    </div>
                                                    <div class="twoStep-spacer">-</div>
                                                    <div class="three-input-groups">
                                                        <input type="number" v-model="otpPart4" maxlength="1"
                                                            class="otp-input" />
                                                        <input type="number" v-model="otpPart5" maxlength="1"
                                                            class="otp-input" />
                                                        <input type="number" v-model="otpPart6" maxlength="1"
                                                            class="otp-input" />
                                                    </div>
                                                </div>
                                            </div>
                                            <div class="mt-4">
                                                <button class="btn btn-primary w-100" type="submit"
                                                    >Verify Phone Number</button>
                                            </div>
                                        </form>
                                    </div>
                                </div>
                                <!-- end card body -->
                            </div>
                            <!-- end card -->
                        </div>
                    </div>
    </template>