laravel-mfa maintained by olusegun171
laravel-mfa
Multi-factor authentication for Laravel. Works with Google Authenticator, Authy, 1Password, Bitwarden, and any other RFC 6238 compatible app.
Features
- TOTP codes — RFC 6238 compliant, 6-digit, 30-second window
- QR code URI generation for any authenticator app
- AES-256-CBC encrypted secret storage
- 8 bcrypt-hashed one-time recovery codes
- Clock-drift tolerance (±1 time-step)
TwoFactorfacade +HasTwoFactorEloquent trait
Requirements
- PHP 8.1+
- Laravel 10, 11, or 12
Installation
composer require olusegun171/laravel-mfa
The service provider and TwoFactor facade are registered automatically via package auto-discovery.
Setup
1. Publish the config
php artisan vendor:publish --tag=two-factor-config
2. Generate the migration
# Resolves the table from the guard's Eloquent model automatically
php artisan two-factor:install --guard=web
# Or pass the table directly
php artisan two-factor:install --table=admins
php artisan migrate
Adds three nullable columns to your table:
two_factor_secret — AES-256-CBC encrypted TOTP secret
two_factor_recovery_codes — JSON array of bcrypt-hashed one-time backup codes
two_factor_confirmed_at — timestamp set when the user confirms their first code
3. Add the trait to your model
use Olusegun171\TwoFactor\Traits\HasTwoFactor;
class User extends Authenticatable
{
use HasTwoFactor;
}
Usage
Enable 2FA (setup flow)
use Olusegun171\TwoFactor\Facades\TwoFactor;
$setup = TwoFactor::setup($user);
// $setup contains:
// [
// 'secret' => 'BASE32SECRET',
// 'qr_code_url' => 'https://api.qrserver.com/...',
// 'otp_auth_uri' => 'otpauth://totp/...',
// 'recovery_codes' => ['XXXX-XXXX-XXXX', ...], // show once, never again
// ]
// Display $setup['qr_code_url'] as an <img src="...">
// Show recovery codes to the user — they won't be shown again
The encrypted secret and hashed recovery codes are saved to the database immediately. two_factor_confirmed_at is null until the user confirms.
Confirm setup
use Olusegun171\TwoFactor\Exceptions\InvalidCodeException;
try {
TwoFactor::confirm($user, $request->code);
// two_factor_confirmed_at is now set — 2FA is active
} catch (InvalidCodeException $e) {
return back()->withErrors(['code' => $e->getMessage()]);
}
Login challenge
After verifying the user's password, check if 2FA is required:
if (TwoFactor::isEnabled($user)) {
// Show the 2FA challenge form, then on submission:
try {
TwoFactor::verify($user, $request->code);
// Code is valid — complete the login
} catch (InvalidCodeException $e) {
return back()->withErrors(['code' => $e->getMessage()]);
}
}
Recovery code fallback
try {
TwoFactor::verifyRecoveryCode($user, $request->recovery_code);
// Code accepted and permanently invalidated — complete the login
} catch (InvalidCodeException $e) {
return back()->withErrors(['code' => $e->getMessage()]);
}
Disable 2FA
TwoFactor::disable($user);
// Clears two_factor_secret, two_factor_recovery_codes, two_factor_confirmed_at
Regenerate recovery codes
$codes = TwoFactor::regenerateRecoveryCodes($user); // string[]
// Old codes are invalidated immediately — show the new ones to the user once
Status Helpers
TwoFactor::isEnabled($user); // true once two_factor_confirmed_at is set
TwoFactor::isPending($user); // true if setup was started but not yet confirmed
TwoFactor::remainingRecoveryCodes($user); // int — unused codes remaining
// On the model (via HasTwoFactor trait)
$user->hasTwoFactorEnabled();
$user->hasTwoFactorPending();
QR Code Identifier
By default, the label embedded in the QR code (and shown in authenticator apps) uses getAuthIdentifier() — typically the user's primary key.
To use a friendlier value such as an email address, define getTwoFactorIdentifier() on your model:
class User extends Authenticatable
{
use HasTwoFactor;
public function getTwoFactorIdentifier(): string
{
return $this->email;
}
}
The returned value is used as the account label in the otpauth:// URI, so it appears as YourApp:user@example.com inside the authenticator app.
Configuration
// config/two-factor.php
return [
'issuer' => env('MFA_ISSUER', null), // shown in authenticator apps; defaults to app name
'totp' => [
'digits' => 6,
'period' => 30, // seconds per time-step
'window' => 1, // ±1 period tolerance for clock drift
'algorithm' => 'sha1',
],
];
Security Notes
- Rate-limit the challenge endpoint — 5 attempts per minute is a reasonable starting point.
- Serve over HTTPS — codes in transit must be encrypted.
- Recovery codes are shown once — only bcrypt hashes are stored.
- All comparisons use
hash_equals()for constant-time evaluation. - TOTP secrets are encrypted with AES-256-CBC using a 32-byte slice of your
APP_KEY. - Never log
two_factor_secretortwo_factor_recovery_codes.
License
MIT — see LICENSE