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: September 2026

Password-only login is still the fastest route to account takeover on production Laravel apps. A Laravel Two-Factor Authentication Complete Setup closes that gap with a second verification step before sensitive sessions or API tokens are issued. This guide walks through Fortify on Laravel 13.x with PHP 8.3+, from installation to TOTP provisioning, recovery codes, and Sanctum API integration. If you are building client portals or payment flows, pair this with the patterns in the ultimate guide to building secure authentication systems.

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

Laravel Fortify is headless authentication scaffolding. It ships controllers and actions for 2FA without forcing a UI kit. You wire the endpoints into Blade, Livewire, or a SPA frontend. Fortify fits monoliths and API-first apps that still need browser-based account management.

Installation and provider registration

Install Fortify with Composer 2.10 on Laravel 13.x running PHP 8.3 or higher:

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

This publishes config/fortify.php and the migration that adds two-factor columns to your users table. Run migrations immediately:

php artisan migrate

Register the Fortify service provider in bootstrap/providers.php on Laravel 12 and 13:

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

Enabling 2FA features in configuration

Open config/fortify.php and enable the twoFactorAuthentication feature 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 value sets TOTP drift tolerance. A value of 1 accepts the current 30-second period plus one before and after. That covers most mobile clock skew. Setting 0 is stricter but increases failed logins when device clocks drift.

Official feature flags and options are documented in the Laravel Fortify documentation.

Fortify 2FA Install PathComposerlaravel/fortifyPublishConfig + MigrationMigrateUser 2FA columnsEnableconfig/fortify.phpAdd TwoFactorAuthenticatable TraitRequired on User model
Laravel Two-Factor Authentication Complete Setup starts with Fortify install, migration, feature enablement, and the User model trait

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 methods such as enableTwoFactorAuthentication(), disableTwoFactorAuthentication(), and recoveryCodes(). Without it, Fortify 2FA routes return 404 even when the feature is enabled.

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

Time-Based One-Time Passwords follow RFC 6238. Authenticator apps derive a six-digit code from a shared secret and the current Unix timestamp in 30-second steps. Understanding that model prevents bugs around clock drift and code reuse.

Secret generation and QR code provisioning

When a user enables 2FA, Fortify generates a random secret and stores it encrypted. The provisioning URI uses the otpauth format:

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

Generate QR codes server-side with bacon/bacon-qr-code, which Fortify pulls in automatically. Server-side generation keeps the raw secret out of browser devtools:

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

Always show recovery codes during initial setup. Users who lose their authenticator without saved codes need admin reset, which does not scale. For test accounts, generate strong placeholder passwords with the password generator tool before enabling 2FA in staging.

Challenge-response verification middleware

Fortify intercepts post-login requests when 2FA is enabled but the session is not yet confirmed. 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 that require a verified 2FA session. The two_factor_confirmed_at session key persists confirmation for the session lifetime. See secure session cookie configuration for cookie flags that protect this state.

TOTP Challenge FlowLoginEmail + Password2FA CheckSession flag?ChallengeEnter TOTPVerifyHMAC matchSet Confirmedtwo_factor_confirmed_atGrant AccessProtected routesFailed codes hit rate limiter — see rate-limiting guide
TOTP challenge sequence: login checks session state, unconfirmed users enter a code, verification sets the session flag for protected access

Rate limiting and brute-force protection

TOTP codes have only one million values per 30-second window. Without throttling, brute force is feasible. Fortify applies rate limiting on the challenge endpoint. Tighten it in your FortifyServiceProvider:

/* 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 password failures. A spike on one account often signals takeover, not user error. Pair this with the tactics in rate limiting to stop brute-force attacks.

How do you implement recovery codes and backup authentication methods?

Recovery codes are the last-resort factor when a user loses their authenticator device. Weak codes or poor UX here create either security holes or permanent lockouts.

Generating and storing recovery codes securely

Fortify generates alphanumeric recovery codes when 2FA is enabled. They are hashed with bcrypt before storage, so a database leak does not 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 at setup. Offer a regenerate endpoint that invalidates old codes atomically. Never email recovery codes or log them in plaintext.

Recovery code verification logic

Add a controller action that accepts a recovery code instead of a TOTP value:

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 enforces single-use semantics. Prompt users to regenerate the full set after any recovery code is consumed.

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

API clients cannot complete browser challenge forms. Treat 2FA as a discrete step that returns structured JSON. When designing token flows, follow Laravel API best practices and compare token types in Laravel Sanctum vs Passport.

API login flow with 2FA challenge

Modify your API login endpoint to return a challenge token when 2FA is enabled:

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()) {
        $challengeToken = Str::random(64);

        Cache::put("2fa_challenge:{$challengeToken}", $user->id, 300);

        return response()->json([
            'two_factor_required' => true,
            'challenge_token' => $challengeToken,
        ], 200);
    }

    $token = $user->createToken('api-token')->plainTextToken;

    return response()->json(['token' => $token]);
}

The challenge token expires after five minutes. Store it in Redis 8.10 for automatic TTL and horizontal scaling. See Redis caching for Laravel for production cache setup.

Confirming 2FA via API endpoint

Create an endpoint that accepts 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)) {
        $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 keeps standard Sanctum token issuance intact while adding 2FA as a pre-flight step. Mobile apps and SPAs consume the same flow. For a full token-based login walkthrough, see building a REST API with Laravel Sanctum authentication.

Browser vs API 2FABrowserBlade / LivewireAPI ClientMobile / SPAFortify BackendSession or token checkTOTP VerifyHMAC + windowIssue CredentialSession or Sanctum
Browser and API clients use different transport but share the same Fortify TOTP verification and credential issuance logic

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

Shipping 2FA without edge-case handling creates false security. On legal-tech portals I have built, skipping session regeneration or step-up auth would fail client confidentiality expectations regardless of checkbox compliance.

Preventing session fixation after 2FA confirmation

Regenerate the session ID immediately after successful 2FA verification:

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

This blocks session fixation where an attacker pre-seeds a cookie before the victim completes authentication. For API flows, consume the challenge token atomically with token issuance.

Enforcing 2FA re-verification for sensitive operations

Long-lived sessions should not grant unlimited access to high-risk actions. Require fresh 2FA confirmation for password changes, recovery code regeneration, or privileged 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 */
}

A 15-minute window balances security and friction. Financial apps may use five minutes. Internal tools might allow thirty. Align this with Laravel policies and gates for role-based step-up rules.

Monitoring and alerting on 2FA anomalies

Log structured events at every verification point: user ID, IP, user agent, success or failure, and method (TOTP vs recovery). Watch for these patterns:

  • Multiple failed TOTP attempts followed by recovery code success suggests takeover
  • 2FA success from geographically impossible locations indicates stolen credentials
  • Rapid recovery code regeneration may signal social engineering against support
  • New device enrollment right after a password reset warrants manual review

Broader threat context lives in cybersecurity trends for developers in 2026. For OWASP-aligned hardening beyond 2FA, read secure Laravel OWASP Top 10 in practice.

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
2FA Hardening Priorities2FA Enabled?Critical FirstRate limit + session regenHigh NextStep-up + monitoringShip to ProductionTest on real devicesAudit QuarterlyReview logs + recovery flow
Production Laravel Two-Factor Authentication Complete Setup: deploy critical controls first, then step-up auth and monitoring before full rollout

On client portals such as Mijar Law Associates, document upload and payment areas demand 2FA on admin accounts at minimum. For full auth architecture on enterprise apps, see enterprise application development services.

Before go-live, run through the Laravel production deployment checklist and confirm password hashing uses Argon2id per Laravel password rehashing guidance. Deploy behind Nginx with TLS 1.3 as covered in deploying Laravel on Ubuntu with Nginx.

Key Takeaways

  • Install Fortify, enable twoFactorAuthentication, migrate, and add TwoFactorAuthenticatable to User before writing any UI.
  • Display recovery codes once at setup and enforce single-use semantics with replaceRecoveryCode().
  • Return a short-lived challenge token for API clients instead of issuing Sanctum tokens before TOTP verification.
  • Regenerate sessions after 2FA confirmation and rate-limit the challenge endpoint to block brute force.
  • Require step-up 2FA for password changes, token creation, and recovery code regeneration.
  • Test with real authenticator apps on iOS and Android — emulators hide clock drift bugs.

People Also Ask

Does Laravel have built-in two-factor authentication?

Laravel does not ship 2FA in the framework core. The official path is Laravel Fortify, a headless package that adds TOTP endpoints, recovery codes, and challenge controllers. Jetstream uses Fortify under the hood if you want pre-built UI.

Which authenticator apps work with Laravel Fortify TOTP?

Any RFC 6238-compatible app works: Google Authenticator, Authy, 1Password, Bitwarden, and Microsoft Authenticator. Fortify generates standard otpauth URIs with six digits and a 30-second period.

Can you use 2FA with Laravel Sanctum API tokens?

Yes. Issue a temporary challenge token after password validation, then exchange a valid TOTP code for a Sanctum personal access token. Never return a long-lived API token until the second factor passes.

What happens if a user loses their authenticator and recovery codes?

They need an admin-initiated reset: disable 2FA on the account after identity verification, then require re-enrollment. Document this process before launch to avoid ad-hoc support decisions under pressure.

Deploy Laravel Two-Factor Authentication With Confidence

A Laravel Two-Factor Authentication Complete Setup using Fortify gives you standards-compliant TOTP, hashed recovery codes, and a clear path for both browser sessions and Sanctum APIs. Start with installation and the challenge middleware, then add API tokens and hardening based on your threat model. Test on physical devices before enabling for all users. For a broader auth overview, read the two-factor auth implementation guide and modern Laravel architecture best practices. Need help auditing or implementing 2FA on a production app? Reach out through my contact page or request a consultation to discuss your authentication 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

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: