feat: enterprise MFA with TOTP, email codes, backup codes, and trusted devices

Three verification methods (TOTP authenticator, email code, backup codes),
trusted device management with 30-day expiry, role-based enforcement for
super_admin and org_admin, admin reset capability, and full test coverage
(46 tests). Modifies login flow to support MFA challenge/response with
temporary session tokens stored in cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 20:45:55 +02:00
parent df68aa8aef
commit 948687f27e
32 changed files with 2563 additions and 5 deletions

View File

@@ -44,6 +44,9 @@ use App\Http\Controllers\Api\V1\Admin\AdminUserController;
use App\Http\Controllers\Api\V1\Admin\AdminStatsController;
use App\Http\Controllers\Api\V1\Admin\AdminActivityLogController;
use App\Http\Controllers\Api\V1\Admin\AdminImpersonationController;
use App\Http\Controllers\Api\V1\Auth\MfaSetupController;
use App\Http\Controllers\Api\V1\Auth\MfaVerifyController;
use App\Http\Controllers\Api\V1\Auth\TrustedDeviceController;
use App\Models\FestivalSection;
use App\Models\Organisation;
use Illuminate\Support\Facades\Gate;
@@ -68,6 +71,10 @@ Route::get('/', fn () => response()->json([
// Public auth routes
Route::post('auth/login', LoginController::class)->middleware('throttle:5,1');
// MFA verification during login (NO auth middleware — uses session token)
Route::post('auth/mfa/verify', [MfaVerifyController::class, 'verify'])->middleware('throttle:10,1');
Route::post('auth/mfa/email/send', [MfaVerifyController::class, 'sendEmailCode'])->middleware('throttle:5,1');
// Public invitation routes (no auth required)
Route::get('invitations/{token}', [InvitationController::class, 'show'])->middleware('throttle:10,1');
Route::post('invitations/{token}/accept', [InvitationController::class, 'accept'])->middleware('throttle:10,1');
@@ -100,6 +107,7 @@ Route::prefix('admin')
// Users
Route::apiResource('users', AdminUserController::class)
->except(['store']);
Route::post('users/{user}/reset-mfa', [AdminUserController::class, 'resetMfa']);
// Platform statistics
Route::get('stats', [AdminStatsController::class, 'index']);
@@ -121,6 +129,20 @@ Route::middleware('auth:sanctum')->group(function () {
Route::post('auth/logout', LogoutController::class);
Route::post('auth/refresh', AuthRefreshController::class);
// MFA setup and management (authenticated)
Route::post('auth/mfa/setup/totp', [MfaSetupController::class, 'setupTotp']);
Route::post('auth/mfa/setup/totp/confirm', [MfaSetupController::class, 'confirmTotp']);
Route::post('auth/mfa/setup/email', [MfaSetupController::class, 'setupEmail']);
Route::post('auth/mfa/setup/email/confirm', [MfaSetupController::class, 'confirmEmail']);
Route::post('auth/mfa/disable', [MfaSetupController::class, 'disable']);
Route::post('auth/mfa/backup-codes', [MfaSetupController::class, 'regenerateBackupCodes']);
Route::get('auth/mfa/status', [MfaSetupController::class, 'status']);
// Trusted devices
Route::get('auth/trusted-devices', [TrustedDeviceController::class, 'index']);
Route::delete('auth/trusted-devices/{device}', [TrustedDeviceController::class, 'destroy']);
Route::delete('auth/trusted-devices', [TrustedDeviceController::class, 'destroyAll']);
// Account management (self-service)
Route::post('me/change-password', [AccountController::class, 'changePassword']);
Route::post('me/change-email', [EmailChangeController::class, 'request']);