Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

knached99's avatar

Laravel Fortify 2fa not authenticating

Hey everyone, I have a Laravel 10.10 application scaffolded with Laravel Breeze and using the ReactJS front end. Since Breeze does not come with two-factor authentication, I have attempted to create the UI for it and use the endpoints provided by Fortify. So just for context, I'm not using the default User model to apply the 2fa functionality. I have created a model called Faculty and need 2fa functionality for that model. Everything but this: https://laravel.com/docs/10.x/fortify#authenticating-with-two-factor-authentication works.

The routes to enable, disable, confirm setting up 2fa, generating the QR code, and backup recvoery codes all work. I just cannot authenticate with 2fa.

Here's a brief part of the front end for the two-factor-challenge route:

export default function TwoFactorChallenge({ message }) {
    
    const { data, setData, post, processing, reset } = useForm({
      code: ''

    });

        const [error, setError] = useState(null);
       
    const submit = (e) => {
        e.preventDefault();
        
        post(route('two-factor.login'));
    };

Here's the backend logic for authentication:

    public function authenticate(Request $request)
{
    try {

        $ip = Faculty::where('email', $request->email)->value('client_ip');
        if($ip){
            $decryptedIP = Crypt::decryptString($ip);
        }
        else{
            $decryptedIP = null;
        }
      
        
        $request->validate([
            'email' => 'required|email|exists:faculty',
            'password' => 'required',
        ], [
            'email.required' => 'Your email is required',
            'email.email' => 'You\'ve entered an invalid email',
            'email.exists' => 'An account for that email does not exist',
            'password.required' => 'Your password is required',
        ]);

        $user = Faculty::where('email', $request->email)->first();
        $banned = $this->isBanned($user->faculty_id);

        if ($banned !== null) {
            return redirect()->back()->withErrors(['auth_error'=>$banned]);
        }
       
        $rateLimitKey = $request->ip();
        $remainingAttempts = 5 - RateLimiter::attempts($rateLimitKey);

        // Set the lockout duration to 10 minutes (600 seconds)
        $lockoutDuration = 600;

        if (RateLimiter::tooManyAttempts($rateLimitKey, 5)) {
            $minutesRemaining = ceil(RateLimiter::availableIn($rateLimitKey) / 60);
            return $this->rateLimitExceededResponse($minutesRemaining);
        }

        $rememberMe = $request->input('remember');

        if (Auth::guard('faculty')->attempt(['email' => $request->input('email'), 'password' => $request->input('password')], $rememberMe)) {
            
            $user = Auth::guard('faculty')->user();

            // if ($user->two_factor_secret && $user->two_factor_confirmed_at !== null) {
            //      return redirect('auth/two-factor-challenge');
            
            //   }

            if ($user->two_factor_secret && $user->two_factor_confirmed_at !== null) {
                // Return an Inertia redirect to the two-factor challenge page
                return Inertia::location('/auth/two-factor-challenge');
            }

           
            if ($rememberMe) {
                $encryptedEmail = Crypt::encryptString($request->input('email'));
                $encryptedPassword = Crypt::encryptString($request->input('password'));

                // Use secure cookies instead of regular cookies
                cookie()->queue('email', $encryptedEmail, 60); // 60 minutes
                cookie()->queue('password', $encryptedPassword, 60);
            }


            if ($ip !== null) {

                if ($decryptedIP !== $request->ip()) {

                    $saveIP = Faculty::where('email', $request->email)->update(['client_ip' => Crypt::encryptString($request->ip())]);

                }
            } else {
                $saveIP = Faculty::where('email', $request->email)->update(['client_ip' => Crypt::encryptString($request->ip())]);
            }
            

            RateLimiter::clear($rateLimitKey);
            $request->session()->regenerate();
            
            return redirect()->intended(RouteServiceProvider::DASH);
            
        } else {
            RateLimiter::hit($rateLimitKey, $lockoutDuration);

            // If decrypted IP is not equal to current IP, then log failed attempt with location 

            if($decryptedIP !== $request->ip()){

            $userAgent = $request->header('User-Agent');

         
            // Get Approximate Location 

        $url = "https://nordvpn.com/wp-admin/admin-ajax.php?action=get_user_info_data&ip={$request->ip()}";

        // Fetch the JSON response using file_get_contents
        $response = file_get_contents($url);

        if ($response === false) {
            // Error fetching data
            $data = null;
        } else {
            // Parse JSON response
            $locationData = json_decode($response);
            $latitude = isset($locationData->coordinates) && is_object($locationData->coordinates) && isset($locationData->coordinates->latitude) ? $locationData->coordinates->latitude : '';
            $longitude = isset($locationData->coordinates) && is_object($locationData->coordinates) && isset($locationData->coordinates->longitude) ? $locationData->coordinates->longitude : '';
            // Build data array
            $data = [
                'email_used' => $request->email,
                'client_ip' => Crypt::encryptString($request->ip()),
                'user_agent' => $userAgent,
                'location_information' => $locationData ?
                    ($locationData->city ?? '') . ', ' .
                    ($locationData->region ?? '') . ', ' .
                    ($locationData->area_code ?? '') . ', ' .
                    ($locationData->country ?? '') . ', ' .
                    ($locationData->timezone ?? '') . ', '. 
                    ($latitude ?? '') . ', ' . 
                    ($longitude ?? '') : 
                    null,
                    'google_maps_link'=>"https://www.google.com/maps?q=$latitude,$longitude",
                    'google_earth_link'=>"https://earth.google.com/web/@$latitude,$longitude,1000a,41407.87820565d,1y,0h,0t,0r",
            ];
        }


            LoginAttempts::updateOrCreate(['client_ip' => Crypt::encryptString($request->ip())], $data);

        }

            return redirect()->back()->withErrors(['auth_error' => 'Your login credentials do not match our records. You have ' . $remainingAttempts . ' attempts remaining before your account gets locked out for 10 minutes']);
        }
    
    } catch (ValidationException $e) {
        return redirect()->back()->withErrors($e->errors())->withInput();
    } 
}

The logic which checks for whether or not 2fa is enabled and then redirects to the challenge page works. But authenticating with 2fa does not, meaning when I enter the code it does not work.

For reference, I've developed the front end but am hitting these endpoints defined by Fortify:

php artisan route:list | grep two-factor
  GET|HEAD  auth/two-factor-challenge .......................................................................... 
  GET|HEAD  two-factor-challenge two-factor.login › Laravel\Fortify › TwoFactorAuthenticatedSessionController@c…
  POST      two-factor-challenge ............... Laravel\Fortify › TwoFactorAuthenticatedSessionController@store
  POST      user/confirmed-two-factor-authentication two-factor.confirm › Laravel\Fortify › ConfirmedTwoFactorA…
  POST      user/two-factor-authentication two-factor.enable › Laravel\Fortify › TwoFactorAuthenticationControl…
  DELETE    user/two-factor-authentication two-factor.disable › Laravel\Fortify › TwoFactorAuthenticationContro…
  GET|HEAD  user/two-factor-qr-code ...... two-factor.qr-code › Laravel\Fortify › TwoFactorQrCodeController@show
  GET|HEAD  user/two-factor-recovery-codes two-factor.recovery-codes › Laravel\Fortify › RecoveryCodeController…
  POST      user/two-factor-recovery-codes ...................... Laravel\Fortify › RecoveryCodeController@store
  GET|HEAD  user/two-factor-secret-key two-factor.secret-key › Laravel\Fortify › TwoFactorSecretKeyController@s…
0 likes
1 reply

Please or to participate in this conversation.