Overview
SaaS Starter Vue includes built-in two-factor authentication (2FA) using Laravel Fortify’s implementation. This provides an additional layer of security by requiring users to verify their identity with a time-based one-time password (TOTP) in addition to their password.
2FA uses authenticator apps like Google Authenticator, Authy, or 1Password to generate verification codes.
How It Works
2FA requires users to:
Enter their email and password (first factor)
Enter a 6-digit code from their authenticator app (second factor)
Optionally use recovery codes if they lose access to their authenticator
TOTP (Time-Based One-Time Password)
The system uses the TOTP algorithm (RFC 6238) which generates codes that expire every 30 seconds. This is the same standard used by Google Authenticator and other popular authenticator apps.
Configuration
Enable 2FA Feature
2FA is enabled in the Fortify configuration:
// config/fortify.php:146
'features' => [
Features :: twoFactorAuthentication ([
'confirm' => true ,
'confirmPassword' => true ,
]),
],
Options:
Require users to confirm 2FA setup with a valid code before activation
Require password confirmation before enabling/disabling 2FA
Database Schema
The users table includes 2FA columns:
// database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php:14
Schema :: table ( 'users' , function ( Blueprint $table ) {
$table -> text ( 'two_factor_secret' ) -> after ( 'password' ) -> nullable ();
$table -> text ( 'two_factor_recovery_codes' ) -> after ( 'two_factor_secret' ) -> nullable ();
$table -> timestamp ( 'two_factor_confirmed_at' ) -> after ( 'two_factor_recovery_codes' ) -> nullable ();
});
Encrypted TOTP secret key used to generate verification codes
two_factor_recovery_codes
Encrypted JSON array of one-time recovery codes
Timestamp when 2FA was successfully confirmed and activated
User Model Setup
The User model includes the TwoFactorAuthenticatable trait:
// app/Models/System/User.php:12
class User extends Authenticatable
{
use HasApiTokens , HasFactory , Notifiable , TwoFactorAuthenticatable ;
protected $hidden = [
'password' ,
'two_factor_secret' ,
'two_factor_recovery_codes' ,
'remember_token' ,
];
protected function casts () : array
{
return [
'email_verified_at' => 'datetime' ,
'password' => 'hashed' ,
'two_factor_confirmed_at' => 'datetime' ,
];
}
}
Never expose two_factor_secret or two_factor_recovery_codes in API responses. They are automatically hidden.
Enabling 2FA
Step-by-Step Setup
Navigate to Security Settings
Users access their security settings page (typically /settings/security)
Confirm Password
User enters their current password to authorize 2FA changes
Enable Two-Factor
User clicks “Enable Two-Factor Authentication” POST / auth / user / two - factor - authentication
Scan QR Code
System generates and displays a QR code. User scans it with their authenticator app GET / auth / user / two - factor - qr - code
Verify Setup
User enters a code from their authenticator app to confirm setup
Save Recovery Codes
System generates 8 recovery codes. User must save them securely GET / auth / user / two - factor - recovery - codes
API Endpoints
Enable 2FA
// routes/web.php:215
Route :: post ( '/user/two-factor-authentication' , [ TwoFactorAuthenticationController :: class , 'store' ])
-> middleware ( $twoFactorMiddleware )
-> name ( 'two-factor.enable' );
This endpoint:
Generates a new TOTP secret
Creates recovery codes
Stores encrypted secret in database
Get QR Code
// routes/web.php:227
Route :: get ( '/user/two-factor-qr-code' , [ TwoFactorQrCodeController :: class , 'show' ])
-> middleware ( $twoFactorMiddleware )
-> name ( 'two-factor.qr-code' );
Returns an SVG QR code that users scan with their authenticator app.
Get Secret Key
// routes/web.php:231
Route :: get ( '/user/two-factor-secret-key' , [ TwoFactorSecretKeyController :: class , 'show' ])
-> middleware ( $twoFactorMiddleware )
-> name ( 'two-factor.secret-key' );
Provides the secret key as text for manual entry if QR scanning isn’t available.
Get Recovery Codes
// routes/web.php:235
Route :: get ( '/user/two-factor-recovery-codes' , [ RecoveryCodeController :: class , 'index' ])
-> middleware ( $twoFactorMiddleware )
-> name ( 'two-factor.recovery-codes' );
Displays the recovery codes for users to save.
Regenerate Recovery Codes
// routes/web.php:219
Route :: post ( '/user/two-factor-recovery-codes' , [ RecoveryCodeController :: class , 'store' ])
-> middleware ( $twoFactorMiddleware )
-> name ( 'two-factor.recovery-codes.store' );
Generates new recovery codes (invalidates old ones).
Disabling 2FA
Users can disable 2FA:
// routes/web.php:223
Route :: delete ( '/user/two-factor-authentication' , [ TwoFactorAuthenticationController :: class , 'destroy' ])
-> middleware ( $twoFactorMiddleware )
-> name ( 'two-factor.disable' );
Navigate to Security Settings
Access the security settings page
Confirm Password
Enter current password to authorize the change
Disable 2FA
Click “Disable Two-Factor Authentication”
Confirmation
2FA is immediately disabled and secret/codes are removed from database
Disabling 2FA reduces account security. Warn users before allowing them to disable it, especially for admin accounts.
Login with 2FA
Two-Factor Challenge Flow
When 2FA is enabled, the login process changes:
Enter Credentials
User enters email and password at /auth/login
Redirect to 2FA Challenge
Instead of logging in, user is redirected to /auth/two-factor-challenge // routes/web.php:203
Route :: get ( '/two-factor-challenge' , [ TwoFactorAuthenticatedSessionController :: class , 'create' ])
-> middleware ([ 'guest:' . config ( 'fortify.guard' )])
-> name ( 'two-factor.login' );
Enter Verification Code
User enters the 6-digit code from their authenticator app (or a recovery code)
Submit Challenge
POST / auth / two - factor - challenge
With either: or { "recovery_code" : "recovery-code-here" }
Login Successful
User is authenticated and redirected to dashboard
Challenge Endpoint
// routes/web.php:208
Route :: post ( '/two-factor-challenge' , [ TwoFactorAuthenticatedSessionController :: class , 'store' ])
-> middleware ([ 'guest:' . config ( 'fortify.guard' )]);
Request Body (Code):
Request Body (Recovery Code):
{
"recovery_code" : "ABCD-EFGH-IJKL-MNOP"
}
Rate Limiting
2FA challenges are rate-limited separately:
// config/fortify.php:117
'limiters' => [
'login' => 'login' ,
'two-factor' => 'two-factor' ,
],
This prevents brute force attacks on verification codes.
Recovery Codes
What Are Recovery Codes?
Recovery codes are one-time use backup codes that allow users to access their account if they:
Lose their authenticator device
Can’t access their authenticator app
Need emergency access
By default, Laravel Fortify generates 8 recovery codes that look like:
ABCD-EFGH-IJKL-MNOP
QRST-UVWX-YZAB-CDEF
...
Using Recovery Codes
Users can enter a recovery code instead of a TOTP code during login:
Click “Use recovery code” on the 2FA challenge page
Enter one of their saved recovery codes
Code is validated and invalidated (can only be used once)
User is logged in
Once a recovery code is used, it’s permanently invalidated. Users should regenerate recovery codes after using them.
Regenerating Recovery Codes
Users should regenerate codes if:
They’ve used some recovery codes
They suspect codes were compromised
They want to invalidate old codes
POST / auth / user / two - factor - recovery - codes
Security Considerations
Password Confirmation
Sensitive 2FA operations require password confirmation:
// routes/web.php:211
$twoFactorMiddleware = Features :: optionEnabled ( Features :: twoFactorAuthentication (), 'confirmPassword' )
? [ config ( 'fortify.auth_middleware' , 'auth' ) . ':' . config ( 'fortify.guard' ), 'password.confirm' ]
: [ config ( 'fortify.auth_middleware' , 'auth' ) . ':' . config ( 'fortify.guard' )];
This prevents attackers from enabling/disabling 2FA if they gain temporary access to an authenticated session.
Best Practices
Critical Security Recommendations:
Always require password confirmation for 2FA changes
Force 2FA for admin/privileged accounts
Educate users to save recovery codes securely
Monitor failed 2FA attempts for suspicious activity
Consider backup authentication methods
Enforcing 2FA
You can require 2FA for specific users or roles:
// Middleware to enforce 2FA
class RequireTwoFactorAuth
{
public function handle ( $request , Closure $next )
{
$user = $request -> user ();
if ( ! $user -> two_factor_secret ) {
return redirect () -> route ( 'settings.security' )
-> with ( 'error' , 'You must enable two-factor authentication to access this resource.' );
}
return $next ( $request );
}
}
Apply to sensitive routes:
Route :: middleware ([ 'auth' , 'require.2fa' ]) -> group ( function () {
Route :: get ( '/admin' , [ AdminController :: class , 'index' ]);
});
Checking 2FA Status
In your blade/Vue templates:
@ if ( auth () -> user () -> two_factor_secret )
< span class = "badge badge-success" > 2 FA Enabled </ span >
@ else
< span class = "badge badge-warning" > 2 FA Disabled </ span >
@ endif
In controllers:
if ( $user -> two_factor_secret ) {
// 2FA is enabled
}
if ( $user -> two_factor_confirmed_at ) {
// 2FA is confirmed and active
}
Testing 2FA
Test 2FA in your application:
use Laravel\Fortify\Actions\ GenerateNewRecoveryCodes ;
use PragmaRX\Google2FA\ Google2FA ;
// Generate a valid code for testing
$user = User :: factory () -> create ();
$user -> forceFill ([
'two_factor_secret' => encrypt ( $secret = app ( Google2FA :: class ) -> generateSecretKey ()),
]) -> save ();
$code = app ( Google2FA :: class ) -> getCurrentOtp (
decrypt ( $user -> two_factor_secret )
);
// Test login with 2FA
$this -> post ( '/auth/login' , [
'email' => $user -> email ,
'password' => 'password' ,
]);
$this -> post ( '/auth/two-factor-challenge' , [
'code' => $code ,
]) -> assertRedirect ( '/dashboard' );
Troubleshooting
Common causes:
Time sync issues - Ensure device clock is accurate (TOTP is time-based)
Wrong secret - User may have scanned QR code for different account
App issues - Try different authenticator app
Solution: Disable and re-enable 2FA with fresh QR code
Lost authenticator device
Options:
Use a recovery code to log in
Contact support to disable 2FA (requires identity verification)
Use backup authenticator if codes were synced (like Authy)
Prevention: Always save recovery codes when setting up 2FA
Check:
User is authenticated
2FA is enabled for the user (two_factor_secret exists)
Route middleware is correct
SVG rendering is supported in browser
Customization
You can customize the 2FA experience:
Custom 2FA Views
Create custom Inertia/Vue pages:
return Inertia :: render ( 'Auth/TwoFactorChallenge' , [
'message' => 'Enter the code from your authenticator app' ,
]);
Custom Recovery Code Count
Modify the recovery code generation in a service provider:
use Laravel\Fortify\Actions\ GenerateNewRecoveryCodes ;
app () -> bind ( GenerateNewRecoveryCodes :: class , function () {
return new GenerateNewRecoveryCodes ( 16 ); // Generate 16 codes instead of 8
});
Backup Methods
Consider implementing backup authentication methods:
SMS verification (requires additional package)
Email verification codes
Hardware security keys (WebAuthn)