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:
@@ -171,3 +171,111 @@ Request
|
||||
| `FRONTEND_APP_URL` | `.env` / `config/app.php` | App SPA origin |
|
||||
| `FRONTEND_PORTAL_URL` | `.env` / `config/app.php` | Portal SPA origin |
|
||||
| `sanctum.expiration` | `config/sanctum.php` | Token TTL (7 days = 10080 minutes) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Multi-Factor Authentication (MFA)
|
||||
|
||||
Crewli supports enterprise-grade MFA with three verification methods, trusted device management, role-based enforcement, and admin reset capability.
|
||||
|
||||
### 9.1 Verification Methods
|
||||
|
||||
| Method | Type | Expiry | Notes |
|
||||
|--------|------|--------|-------|
|
||||
| TOTP | Authenticator app (Google Authenticator, Authy, etc.) | 30s per code | Primary, most secure. Secret stored encrypted. |
|
||||
| Email code | 6-digit code sent via EmailService | 10 min | Fallback for TOTP users, or standalone method. Rate-limited: 1 code per 60s. |
|
||||
| Backup codes | 10 single-use codes (XXXX-XXXX format) | Never | Generated at MFA setup. Stored as bcrypt hashes. Last resort recovery. |
|
||||
|
||||
### 9.2 Login Flow with MFA
|
||||
|
||||
```
|
||||
Client API
|
||||
│ │
|
||||
│ POST /auth/login │
|
||||
│ { email, password } │
|
||||
│ ─────────────────────────────────►│
|
||||
│ │ ── Validate credentials
|
||||
│ │ ── Check MFA enabled?
|
||||
│ │
|
||||
│ ┌── MFA NOT enabled ──────────┤
|
||||
│ │ Return auth token (cookie)│
|
||||
│ │ │
|
||||
│ └── MFA enabled ─────────────┤
|
||||
│ │ │
|
||||
│ ├── Trusted device? ─ YES ─ Return auth token (skip MFA)
|
||||
│ │ │
|
||||
│ └── No trusted device ────┤
|
||||
│ Return mfa_required │
|
||||
│ + mfa_session_token │
|
||||
│ ◄────────────────────────────────│
|
||||
│ │
|
||||
│ POST /auth/mfa/verify │
|
||||
│ { mfa_session_token, code, │
|
||||
│ method, trust_device? } │
|
||||
│ ─────────────────────────────────►│
|
||||
│ │ ── Verify code
|
||||
│ │ ── Optionally trust device
|
||||
│ ◄── auth token (cookie) ─────────│
|
||||
```
|
||||
|
||||
### 9.3 MFA Session
|
||||
|
||||
After successful password authentication, if MFA is required, the API creates a temporary MFA session:
|
||||
- Stored in Redis/Cache with prefix `mfa_session:`
|
||||
- TTL: 10 minutes
|
||||
- Contains: `user_id`, `ip_address`, `created_at`
|
||||
- IP address is checked on verification — if it changes, the session is invalidated
|
||||
- Session is consumed (deleted) after successful MFA verification
|
||||
|
||||
### 9.4 Trusted Devices
|
||||
|
||||
Users can opt to trust a device during MFA verification. Trusted devices:
|
||||
- Skip MFA on subsequent logins for 30 days
|
||||
- Are identified by a SHA-256 hash of `device_fingerprint + user_id`
|
||||
- The `X-Device-Fingerprint` header must be sent with login requests
|
||||
- Can be listed, individually revoked, or all revoked by the user
|
||||
- Are stored in the `trusted_devices` table with ULID primary key
|
||||
|
||||
### 9.5 Backup Codes
|
||||
|
||||
- 10 codes generated at MFA setup (format: `XXXX-XXXX`)
|
||||
- Stored as bcrypt hashes — plain codes shown to user only once
|
||||
- Each code is single-use (marked `used` + `used_at` on verification)
|
||||
- Can be regenerated (requires TOTP verification)
|
||||
- Input normalization: spaces and dashes stripped, case-insensitive
|
||||
|
||||
### 9.6 Role-Based Enforcement
|
||||
|
||||
MFA is required (enforced) for:
|
||||
- `super_admin` — always
|
||||
- `org_admin` — always
|
||||
- Any user in an organisation with `settings.enforce_mfa = true`
|
||||
- Any user with `mfa_enforced = true` flag
|
||||
|
||||
When MFA is required but not yet set up, login succeeds but includes `mfa_setup_required: true` flag. The frontend should redirect to MFA setup.
|
||||
|
||||
### 9.7 Admin Reset
|
||||
|
||||
Platform admins (`super_admin`) can force-disable MFA for any user via `POST /admin/users/{user}/reset-mfa`. This:
|
||||
- Clears all MFA data (secret, backup codes, email codes, trusted devices)
|
||||
- Logs the action in the activity log with `mfa.admin_reset` event
|
||||
- The user must re-enable MFA on next login if enforcement policies apply
|
||||
|
||||
### 9.8 Database Tables
|
||||
|
||||
| Table | PK Type | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `users` (MFA columns) | — | `mfa_enabled`, `mfa_method`, `mfa_secret` (encrypted), `mfa_confirmed_at`, `mfa_enforced` |
|
||||
| `mfa_backup_codes` | auto-increment | Hashed single-use recovery codes |
|
||||
| `mfa_email_codes` | auto-increment | Temporary 6-digit email verification codes |
|
||||
| `trusted_devices` | ULID | Device trust records with expiry |
|
||||
|
||||
### 9.9 Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `app/Services/MfaService.php` | Central MFA logic — setup, verification, backup codes, trusted devices, enforcement |
|
||||
| `app/Enums/MfaMethod.php` | TOTP, EMAIL, BACKUP_CODE enum |
|
||||
| `app/Http/Controllers/Api/V1/Auth/MfaSetupController.php` | Authenticated MFA setup/disable/status endpoints |
|
||||
| `app/Http/Controllers/Api/V1/Auth/MfaVerifyController.php` | Login-flow MFA verification (unauthenticated) |
|
||||
| `app/Http/Controllers/Api/V1/Auth/TrustedDeviceController.php` | Trusted device management |
|
||||
|
||||
Reference in New Issue
Block a user