aymakan/laravel-mfa
Laravel MFA Package
A production-grade, API-only Multi-Factor Authentication (MFA) enforcement package for Laravel. This package provides a clean, middleware-driven MFA layer that works after successful email/password login.
Features
- TOTP-based MFA using RFC 6238 (compatible with Google Authenticator, Authy, etc.)
- API-only — no Blade views, no redirects; JSON responses only
- Middleware-driven — does not replace Laravel authentication
- Pluggable architecture — ready for future SMS/Email/Push drivers
- Multiple verification stores — session, cache, or database (see Verification state storage)
- Security-first — OTP replay protection, rate limiting, fail-closed design
- Config-driven — fully customizable via published config
- Enforcement options — global (
required/optional) or per-user (e.g.mfa_requiredattribute)
Requirements
- PHP 8.2+
- Laravel 10+ / 11+ / 12+
- Laravel Sanctum (or similar API auth with Bearer token)
Installation
composer require aymakan/laravel-mfa
Publish config and migrations:
php artisan vendor:publish --tag=mfa-config
php artisan vendor:publish --tag=mfa-migrations
php artisan migrate
Basic setup
1. Add the HasMfa trait to your User model
use Aymakan\Mfa\Traits\HasMfa;
class User extends Authenticatable
{
use HasMfa;
// Optional: add to $fillable if using per-user enforcement
protected $fillable = ['email', 'password', 'mfa_required'];
protected $casts = [
'mfa_required' => 'boolean',
];
}
2. Apply the middleware to protected routes
// routes/api.php (or your API route file)
Route::middleware(['auth:sanctum', 'mfa.verified'])->group(function () {
Route::get('/user', [UserController::class, 'show']);
Route::get('/dashboard', [DashboardController::class, 'index']);
// ... other protected routes
});
MFA routes (status, verify, enroll) are not protected by mfa.verified; they only require auth. Keep your main app routes behind mfa.verified.
API endpoints
The package registers these routes under the configured prefix (default: mfa, so full paths are e.g. /mfa/status):
| Method | Path | Description |
|---|---|---|
| GET | /{prefix}/status |
MFA status for authenticated user |
| POST | /{prefix}/verify |
Verify MFA code (login flow) |
| POST | /{prefix}/enroll/start |
Start MFA enrollment |
| POST | /{prefix}/enroll/confirm |
Confirm enrollment with OTP |
| DELETE | /{prefix}/enroll/cancel |
Cancel pending enrollment |
| DELETE | /{prefix}/disable |
Disable MFA (requires OTP) |
/verify and /enroll/confirm and /disable use the throttle:mfa rate limiter.
Usage examples
Check MFA status
GET /mfa/status
Authorization: Bearer {token}
Response:
{
"data": {
"mfa_enabled": true,
"mfa_verified": true,
"mfa_type": "totp",
"mfa_required": true,
"verified_at": "2026-02-02T12:39:42+00:00"
}
}
verified_at is present only when mfa_verified is true.
Enroll in MFA
Step 1: Start enrollment
POST /mfa/enroll/start
Authorization: Bearer {token}
Response:
{
"data": {
"provisioning_uri": "otpauth://totp/MyApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp",
"type": "totp",
"message": "Scan the QR code with your authenticator app, then confirm with a code."
}
}
Step 2: Generate a QR code from provisioning_uri (e.g. with a frontend library or https://api.qrserver.com).
Step 3: Confirm enrollment
POST /mfa/enroll/confirm
Authorization: Bearer {token}
Content-Type: application/json
{"code": "123456"}
Response:
{
"data": {
"enabled": true,
"message": "MFA has been enabled successfully."
}
}
Verify MFA (login flow)
After login, if protected routes return 403 with MFA_REQUIRED, call verify with the user’s OTP:
POST /mfa/verify
Authorization: Bearer {token}
Content-Type: application/json
{"code": "123456"}
Response:
{
"data": {
"verified": true,
"message": "MFA verification successful."
}
}
Disable MFA
DELETE /mfa/disable
Authorization: Bearer {token}
Content-Type: application/json
{"code": "123456"}
Response:
{
"data": {
"disabled": true,
"message": "MFA has been disabled."
}
}
Middleware behaviour
When mfa.verified is applied:
- If MFA is globally disabled (
enabled= false) → pass through. - If the user is not authenticated → pass through (let auth middleware handle it).
- Enforcement:
- If the user must complete MFA (
enforcement=requiredor per-user attribute e.g.mfa_required= true): they must have MFA enabled and verified; otherwise → 403MFA_REQUIRED. - If MFA is optional for this user: only users who already have MFA enabled must verify; others pass.
- If the user must complete MFA (
- If the user has MFA enabled and is verified → pass through.
- If the user has MFA enabled but not verified → 403
MFA_REQUIRED.
The mfa_required field in GET /mfa/status indicates whether this user must complete MFA (enroll and/or verify).
Error response when MFA is required but not satisfied:
{
"error": {
"code": "MFA_REQUIRED",
"message": "Multi-factor authentication is required."
}
}
Verification state storage
Verification state (“this user has passed MFA for this session / period”) can be stored in three ways:
| Store | Config value | Use when |
|---|---|---|
| Session | session (default) |
Frontend sends session cookies (stateful Sanctum). Verification lives for the session. |
| Cache | cache |
Bearer-only API; no session. Verification is keyed by user id and TTL (e.g. 8 hours). |
| Database | database |
Bearer-only API; survives cache flush. Uses mfa_verifications table and TTL. |
Set in config:
'verification' => [
'store' => env('MFA_VERIFICATION_STORE', 'session'), // 'session' | 'cache' | 'database'
// Session (when store === 'session')
'session_key' => 'mfa_verified_at',
'lifetime' => null, // null = session lifetime
// Cache (when store === 'cache')
'key_prefix' => 'mfa_verified_at',
'lifetime' => 480, // minutes (e.g. 8 hours)
// Database (when store === 'database'); run mfa migrations
'table' => 'mfa_verifications',
'lifetime' => 480,
],
For SPAs using only Bearer tokens (no session cookies), use cache or database so verification persists across requests.
Configuration options
// config/mfa.php
return [
'enabled' => env('MFA_ENABLED', true),
// 'optional' = only users who have MFA enabled must verify
// 'required' = every user must enroll and verify
'enforcement' => env('MFA_ENFORCEMENT', 'optional'),
// Per-user attribute (e.g. mfa_required). When true, that user must complete MFA
// even when enforcement is 'optional'. Set to null to disable.
'enforcement_per_user_attribute' => env('MFA_ENFORCEMENT_PER_USER_ATTRIBUTE'),
'default' => env('MFA_DRIVER', 'totp'),
'drivers' => [
'totp' => [
'issuer' => env('MFA_TOTP_ISSUER', config('app.name')),
'digits' => 6,
'period' => 30,
'algorithm' => 'sha1',
'window' => 1,
],
],
'routes' => [
'prefix' => 'mfa',
'middleware' => ['api', 'auth:sanctum'],
],
'middleware_alias' => 'mfa.verified',
'errors' => [
'mfa_required' => [
'status' => 403,
'code' => 'MFA_REQUIRED',
'message' => 'Multi-factor authentication is required.',
],
'mfa_invalid' => [
'status' => 422,
'code' => 'MFA_INVALID',
'message' => 'Invalid verification code.',
],
'mfa_rate_limited' => [
'status' => 429,
'code' => 'MFA_RATE_LIMITED',
'message' => 'Too many verification attempts. Please try again later.',
],
],
'rate_limit' => [
'max_attempts' => 5,
'decay_minutes' => 5,
],
'verification' => [
'store' => env('MFA_VERIFICATION_STORE', 'session'),
'session_key' => 'mfa_verified_at',
'lifetime' => null,
'key_prefix' => 'mfa_verified_at',
'table' => 'mfa_verifications',
],
'user' => [
'model' => null, // or \App\Models\User::class
'foreign_key' => 'user_id',
],
];
Using the facade
use Aymakan\Mfa\Facades\Mfa;
$hasMfa = Mfa::userHasMfaEnabled($user);
$valid = Mfa::verify($user, '123456');
$uri = Mfa::getProvisioningUri($user);
Mfa::disable($user);
Security
- No OTP reuse — replay protection for verification attempts.
- Rate limiting —
throttle:mfaon verify, confirm, and disable (configurable inrate_limit). - Fail closed — if MFA state is unclear, access is denied.
- Logout — verification state is cleared on
Illuminate\Auth\Events\Logout.
Testing
composer test
License
MIT License
Related Packages
A One Time Password Authentication package, compatible with Google Authenticator...
Laravel authentication with AWS Cognito, supporting web, API, SSO, MFA, WebAuthn...
HMAC API authentication for Laravel with signature verification, replay attack p...
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v1.0.1 | ^8.4 | ^12.0 | MIT | |
| v1.0.0 | ^8.4 | ^12.0 | MIT |