
August 12, 2026
12 min read
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.
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.
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.
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 Measure | Risk Mitigated | Implementation Effort | Production Priority |
|---|---|---|---|
| Session regeneration post-2FA | Session fixation attacks | Low (single line) | Critical |
| Rate limiting on challenge endpoint | Brute-force TOTP guessing | Low (config change) | Critical |
| Step-up auth for sensitive ops | Session hijack escalation | Medium (middleware) | High |
| Recovery code single-use enforcement | Replay attacks | Low (built-in) | Critical |
| Anomaly monitoring and alerting | Undetected account takeover | High (logging pipeline) | High |
| Encrypted secret storage at rest | Database breach exposure | Low (Fortify default) | Critical |
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 addTwoFactorAuthenticatableto 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
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.

