Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Two-Factor Authentication Complete Setup

By Kokil Thapa | Last reviewed: August 2026

Implementing Laravel Two-Factor Authentication Complete Setup is no longer optional for applications handling sensitive user data or financial transactions. Password-only authentication remains the single largest attack vector for account compromise, and modern security standards require a second verification factor to mitigate credential stuffing and phishing. This guide provides a production-ready implementation path using Laravel Fortify, covering TOTP configuration, recovery code management, and API integration patterns verified on Laravel 12.x with PHP 8.4.

How do you install and configure Laravel Fortify for two-factor authentication?

Laravel Fortify serves as the backend authentication scaffolding that powers 2FA without imposing frontend opinions. Unlike older packages that bundled UI components, Fortify provides headless controllers and actions you integrate into your own Blade templates, Livewire components, or Vue/React frontends. For developers evaluating whether to build custom auth or use managed solutions, understanding this foundation is critical before considering alternatives like those discussed in the ultimate guide to building secure authentication systems.

Installation and provider registration

Install Fortify via Composer on a Laravel 12.x application running PHP 8.2 or higher:

composer require laravel/fortify
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"

This publishes the configuration file to config/fortify.php and creates the migration for the two_factor_authentications table. Run migrations immediately:

php artisan migrate

Register the Fortify service provider in bootstrap/providers.php (Laravel 12 structure) or config/app.php (Laravel 11 and earlier):

// bootstrap/providers.php
return [
    App\Providers\AppServiceProvider::class,
    Laravel\Fortify\FortifyServiceProvider::class,
];

Enabling 2FA features in configuration

Open config/fortify.php and ensure the twoFactorAuthentication feature is uncommented in the features array:

'features' => [
    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    Features::twoFactorAuthentication([
        'confirm' => true,
        'confirmPassword' => true,
        'window' => 1,
    ]),
],

The window parameter controls time drift tolerance. A value of 1 accepts the current TOTP period plus one period before and after (total 90-second window). For most production applications, this balances usability against clock skew on mobile devices. Setting it to 0 enforces strict timing but increases failed login rates when device clocks drift more than 30 seconds.

Composer Installlaravel/fortifyPublish AssetsConfig + MigrationRun Migrationtwo_factor tableEnable Featureconfig/fortify.php
Laravel Fortify installation sequence: four sequential steps from package installation to feature activation in configuration

User model trait integration

Add the TwoFactorAuthenticatable trait to your User model:

use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    use TwoFactorAuthenticatable;
    
    // ... existing traits and properties
}

This trait adds relationships and methods for managing 2FA state, including enableTwoFactorAuthentication(), disableTwoFactorAuthentication(), and recoveryCodes(). Without this trait, Fortify's 2FA endpoints return 404 errors regardless of configuration.

What is the TOTP authentication flow and how does verification work?

Time-Based One-Time Passwords follow RFC 6238, generating six-digit codes derived from a shared secret and the current Unix timestamp divided into 30-second periods. Understanding this mechanism prevents common implementation mistakes around clock synchronization and code reuse.

Secret generation and QR code provisioning

When a user enables 2FA, Fortify generates a cryptographically random secret stored encrypted in the database. The provisioning URI follows the otpauth format:

otpauth://totp/Laravel:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Laravel&digits=6&period=30

Generate QR codes server-side using bacon/bacon-qr-code (installed automatically with Fortify) or client-side via JavaScript libraries. Server-side generation prevents exposing the raw secret to browser devtools:

// In your controller or Livewire component
$svg = $user->twoFactorQrCodeSvg();
return response()->view('auth.two-factor-setup', ['qrSvg' => $svg]);

Always display recovery codes alongside the QR code during initial setup. Users who lose access to their authenticator app without saved recovery codes require administrative intervention, creating support overhead that scales poorly.

Challenge-response verification middleware

Fortify intercepts post-login requests when 2FA is enabled but not yet confirmed for the session. Create middleware to enforce confirmation on protected routes:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class ConfirmTwoFactorAuthenticated
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->user() && 
            ! $request->session()->get('two_factor_confirmed_at')) {
            return redirect()->route('two-factor.challenge');
        }

        return $next($request);
    }
}

Register this middleware in bootstrap/app.php and apply it to route groups requiring verified 2FA sessions. The session key two_factor_confirmed_at persists confirmation across page loads within the same session lifetime.

User LoginEmail + Password2FA CheckSession Flag?Show ChallengeEnter TOTP CodeVerify CodeHMAC-SHA1 MatchSet Session Confirmedtwo_factor_confirmed_atGrant AccessProtected Resources
TOTP challenge-response sequence: login triggers session check, unconfirmed users see code entry form, successful verification sets session flag granting access

Rate limiting and brute-force protection

TOTP codes have only one million possible values per period, making them vulnerable to brute-force attacks without throttling. Fortify applies rate limiting automatically via the throttle:6,1 middleware on the challenge endpoint, allowing six attempts per minute. In production environments serving Nepal-based clients where SMS fallback might be used alongside TOTP, consider reducing this to three attempts and implementing exponential backoff:

// In FortifyServiceProvider boot method
RateLimiter::for('two-factor', function (Request $request) {
    return Limit::perMinute(3)->by($request->session()->id());
});

Log failed 2FA attempts separately from general authentication failures. A spike in 2FA failures for a specific account often indicates an active attack rather than user error, warranting temporary account lockout or notification.

How do you implement recovery codes and backup authentication methods?

Recovery codes serve as the last-resort authentication method when users lose access to their primary authenticator device. Poorly implemented recovery flows create either security holes (codes too weak or predictable) or operational nightmares (users permanently locked out).

Generating and storing recovery codes securely

Fortify generates eight-character alphanumeric recovery codes automatically when 2FA is enabled. These are hashed before storage using bcrypt, meaning even database breaches don't expose usable codes:

// Recovery codes are generated during enableTwoFactorAuthentication()
// Access decrypted codes only once during setup
$recoveryCodes = $user->recoveryCodes();

// Store in session flash for one-time display
session()->flash('recoveryCodes', $recoveryCodes);

Display recovery codes exactly once during initial setup. Provide a "Regenerate Recovery Codes" endpoint for users who lose their original set, invalidating previous codes atomically. Never email recovery codes or store them in plaintext logs.

Recovery code verification logic

Create a dedicated controller action for recovery code authentication that bypasses TOTP validation:

public function recoverUsingBackupCode(Request $request)
{
    $request->validate(['code' => 'required|string']);
    
    $user = $request->user();
    
    if (! $user->validRecoveryCode($request->code)) {
        return back()->withErrors(['code' => 'Invalid recovery code.']);
    }
    
    // Invalidate used code to prevent replay
    $user->replaceRecoveryCode($request->code);
    
    session()->put('two_factor_confirmed_at', now());
    
    return redirect()->intended('/dashboard');
}

The replaceRecoveryCode() method removes the used code from the valid set, enforcing single-use semantics. Users should regenerate their full recovery code set after using any individual code, maintaining security hygiene.

How do you integrate 2FA with Laravel Sanctum for API authentication?

API clients cannot interact with browser-based challenge forms, requiring programmatic 2FA confirmation. When building REST APIs following patterns described in Laravel API best practices, handle 2FA as a discrete authentication step returning structured JSON responses.

API login flow with 2FA challenge

Modify your API login endpoint to detect pending 2FA and return a challenge token instead of an access token:

public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (! Auth::attempt($credentials)) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }

    $user = Auth::user();

    if ($user->hasEnabledTwoFactorAuthentication()) {
        // Generate temporary challenge token
        $challengeToken = Str::random(64);
        
        Cache::put("2fa_challenge:{$challengeToken}", $user->id, 300);
        
        return response()->json([
            'two_factor_required' => true,
            'challenge_token' => $challengeToken,
        ], 200);
    }

    // Standard token issuance for non-2FA users
    $token = $user->createToken('api-token')->plainTextToken;
    
    return response()->json(['token' => $token]);
}

The challenge token expires after five minutes (300 seconds), preventing indefinite session hijacking windows. Store these tokens in Redis rather than the database for automatic expiration and horizontal scaling compatibility.

Confirming 2FA via API endpoint

Create a separate endpoint accepting the challenge token and TOTP code:

public function confirmTwoFactor(Request $request)
{
    $request->validate([
        'challenge_token' => 'required|string',
        'code' => 'required|string|size:6',
    ]);

    $userId = Cache::pull("2fa_challenge:{$request->challenge_token}");

    if (! $userId) {
        return response()->json(['message' => 'Challenge expired'], 410);
    }

    $user = User::findOrFail($userId);

    if (! $user->validTwoFactorCode($request->code)) {
        // Re-issue challenge for retry
        $newToken = Str::random(64);
        Cache::put("2fa_challenge:{$newToken}", $userId, 300);
        
        return response()->json([
            'message' => 'Invalid code',
            'challenge_token' => $newToken,
        ], 422);
    }

    $token = $user->createToken('api-token')->plainTextToken;
    
    return response()->json(['token' => $token]);
}

This pattern keeps the standard Sanctum token issuance unchanged while adding 2FA as an optional pre-flight step. Mobile apps and SPAs consume this flow identically to browser clients, maintaining consistent security posture across platforms.

Browser ClientBlade / Livewire FormAPI ClientMobile App / SPAFortify Challenge ControllerSession or Token ValidationTOTP VerificationHMAC-SHA1 + WindowIssue CredentialSession Flag or Sanctum Token
Unified 2FA architecture: browser and API clients use different transport mechanisms but share identical TOTP verification logic and credential issuance patterns

What production security hardening measures prevent common 2FA bypass vulnerabilities?

Shipping 2FA without addressing edge cases creates false security. On legal-tech portals I've built for Nepal law firms handling sensitive case documents, skipping these hardening steps would violate client confidentiality expectations regardless of technical compliance.

Preventing session fixation after 2FA confirmation

Regenerate the session ID immediately after successful 2FA verification to prevent session fixation attacks where an attacker pre-seeds a session cookie before the victim completes authentication:

// After successful 2FA verification
$request->session()->regenerate();
$request->session()->put('two_factor_confirmed_at', now());

This applies equally to browser sessions and API challenge tokens. For Sanctum-based APIs, ensure the challenge token is consumed (deleted from cache) atomically with token issuance to prevent race conditions.

Enforcing 2FA re-verification for sensitive operations

Long-lived sessions shouldn't grant unlimited access to high-risk actions. Implement step-up authentication requiring fresh 2FA confirmation for password changes, recovery code regeneration, or privileged API token creation:

public function updatePassword(Request $request)
{
    $confirmedAt = session('two_factor_confirmed_at');
    
    if (! $confirmedAt || now()->diffInMinutes($confirmedAt) > 15) {
        return response()->json([
            'message' => 'Re-authentication required',
            'step_up_required' => true,
        ], 403);
    }
    
    // Proceed with password update
}

The 15-minute window balances security against friction. Adjust based on your application's risk profile; financial applications may use 5 minutes while internal tools might allow 30.

Monitoring and alerting on 2FA anomalies

Instrument your 2FA verification points with structured logging capturing user ID, IP address, user agent, success/failure status, and method (TOTP vs recovery code). Forward these events to your monitoring stack for anomaly detection:

  • Multiple failed TOTP attempts followed by successful recovery code use suggests account takeover attempt
  • Successful 2FA from geographically impossible locations indicates credential compromise
  • Rapid recovery code regeneration cycles may indicate social engineering targeting support staff
  • New device enrollment immediately after password reset warrants manual review

For teams managing multiple client projects, integrating these signals into centralized dashboards helps identify coordinated attacks across properties. Developers exploring broader security infrastructure should review cybersecurity trends for developers in 2026 for emerging threat patterns affecting authentication systems.

Security MeasureRisk MitigatedImplementation EffortProduction Priority
Session regeneration post-2FASession fixation attacksLow (single line)Critical
Rate limiting on challenge endpointBrute-force TOTP guessingLow (config change)Critical
Step-up auth for sensitive opsSession hijack escalationMedium (middleware)High
Recovery code single-use enforcementReplay attacksLow (built-in)Critical
Anomaly monitoring and alertingUndetected account takeoverHigh (logging pipeline)High
Encrypted secret storage at restDatabase breach exposureLow (Fortify default)Critical

Laravel Two-Factor Authentication Complete Setup Next Steps

The Laravel Two-Factor Authentication Complete Setup outlined here provides a production-grade foundation using Fortify's battle-tested primitives. Start with the base installation and TOTP flow, then layer in API support and hardening measures based on your application's threat model. Test thoroughly with real authenticator apps across iOS and Android before enabling for production users, as emulator time drift masks verification bugs that surface only on physical devices. If you need assistance implementing 2FA for your Laravel application or auditing an existing authentication system, reach out through my contact page to discuss your specific requirements.

Frequently Asked Questions

Install Laravel Fortify via Composer, run migrations, enable the twoFactorAuthentication feature in config/fortify.php, and implement the confirmation view. This provides TOTP and recovery codes without building custom logic.

Yes. Fortify integrates directly with Sanctum for SPA and mobile authentication. After verifying the 2FA challenge via the /two-factor-challenge endpoint, Sanctum issues an authenticated session or token that respects your configured 2FA requirements for subsequent API requests.

For a standard Laravel 12 application, adding Fortify-based 2FA typically costs Rs 25,000–40,000 (USD 190–300). This covers backend configuration, frontend confirmation screens, recovery code handling, and testing. Complex integrations or custom SMS delivery increase the estimate significantly depending on gateway fees and UI requirements.

Fortify defaults to TOTP but supports custom SMS providers through the twoFactorChallengeResponse method. In Nepal, integrating eSewa or Khalti OTP APIs requires writing a custom verification service since no native driver exists. I have found TOTP more reliable than SMS due to carrier delays and message filtering issues common with Nepali telecom providers during peak hours.

Laravel generates ten single-use recovery codes during 2FA setup. Users must store these securely as they bypass TOTP verification entirely. On production systems I maintain, we display these codes once in a modal requiring explicit acknowledgment. If all codes are exhausted, administrators can regenerate them via Artisan or a secure admin panel after identity verification.

Fortify is headless and ideal when you need full control over views or are building an API-first application. Jetstream bundles Fortify with Tailwind CSS views and team management. For legal-tech portals where branding matters, I prefer Fortify alone because it avoids opinionated frontend dependencies while providing identical 2FA security primitives and recovery mechanisms.

Create middleware checking auth()->user()->hasEnabledTwoFactorAuthentication() and redirect unverified users to the setup page. Apply this middleware to protected routes or role-specific route groups. Spatie Laravel Permission pairs well here by restricting enforcement to roles like admin or editor. Always allow graceful opt-out periods before mandatory enforcement to prevent lockouts during rollout phases.

Server time drift is the most common cause. TOTP validates against UTC timestamps with a thirty-second window. Ensure your Ubuntu server runs timedatectl set-ntp true and verify synchronization status. Also confirm users scan QR codes correctly rather than manually entering secrets, as whitespace or case errors invalidate codes. Debugbar helps inspect session state during development troubleshooting.

Absolutely. Fortify encrypts secrets using Laravel's APP_KEY automatically via the EncryptCasts attribute. Never disable this or store plaintext secrets. On shared hosting environments where filesystem permissions are questionable, verify encryption at rest by checking the users table schema. Compromised databases without encrypted secrets expose every account to immediate takeover regardless of password strength.

Yes. Publish Fortify views using php artisan vendor:publish --tag=fortify-views to override blade templates. Modify resources/views/auth/two-factor-challenge.blade.php for styling, error messages, or recovery code input placement. Keep form field names intact to maintain compatibility with Fortify controllers. I typically add inline validation feedback and accessible labels to reduce support tickets from confused users during initial login attempts.

Fortify applies rate limiting automatically via the throttle middleware on /two-factor-challenge endpoints. Default limits allow five attempts per minute per IP. Configure stricter thresholds in RouteServiceProvider for sensitive applications. Failed attempts increment cache counters without revealing whether the code format was correct. Combine this with fail2ban monitoring authentication logs on production servers to block persistent attackers before they exhaust valid recovery codes.

Running php artisan fortify:install adds two_factor_secret, two_factor_recovery_codes, and two_factor_confirmed_at columns to the users table via migration. The secret and recovery codes store encrypted JSON. No additional tables are created. Always backup your users table before migrating production databases. Verify column types match Fortify expectations if upgrading from older Laravel versions where schema definitions differed slightly.

Yes. Two-factor authentication operates independently from password hashing. Authenticated users visit the profile settings page to generate QR codes and confirm their first TOTP code. Existing sessions remain valid until logout unless you explicitly invalidate them via Auth::logoutOtherDevices(). During migrations on client projects, I send email notifications explaining optional enrollment rather than forcing immediate activation to avoid disrupting active workflows.

Use the twoFactorSecret() factory method in tests to generate deterministic secrets. Alternatively, install a browser extension like Authenticator.cc to import test secrets directly. During development, temporarily disable confirmed_at checks in a local-only middleware to skip verification steps. Never commit test bypasses to version control. I keep separate .env.testing configurations ensuring production builds always enforce real TOTP validation regardless of debug settings.

Skipping recovery code generation leaves users permanently locked out after device loss. Not enforcing HTTPS exposes TOTP codes to interception on public networks. Failing to log successful 2FA events prevents forensic analysis during breach investigations. Allowing unlimited recovery code regeneration defeats the purpose of one-time backups. Always audit your implementation against OWASP MFA guidelines and conduct penetration testing before deploying to production environments handling sensitive personal or financial data.

Share this article

Quick Contact Options
Choose how you want to connect me: