feat: replace token-based impersonation with enterprise-grade header-based system

Replaces the insecure token-in-localStorage approach with a header-based
impersonation system backed by cache sessions and MFA verification.

Key changes:
- New impersonation_sessions audit table (immutable, ULID PK)
- MFA verification required to start impersonation (TOTP/email/backup)
- X-Impersonate-User header + HandleImpersonation middleware
- Per-request auth context swap (admin session never modified)
- IP pinning, sensitive route blocking, no nesting, sliding 60-min TTL
- Activity log auto-tagged with impersonated_by during sessions
- Frontend: sessionStorage, BroadcastChannel sync, countdown timer
- ImpersonateDialog with reason + MFA verification flow
- 26 comprehensive tests covering core, middleware, audit, lifecycle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 02:42:53 +02:00
parent 47cb6b83d4
commit 4df668b5b8
25 changed files with 1813 additions and 269 deletions

View File

@@ -926,17 +926,49 @@ Base path: `/api/v1/admin/`
### Admin Impersonation
- `POST /admin/impersonate/{user}` — start impersonating a user (requires `role:super_admin`)
- `POST /admin/stop-impersonation` — stop impersonation (requires `auth:sanctum` only, callable by impersonated user)
Header-based impersonation with MFA verification. See `AUTH_ARCHITECTURE.md` section 10 for full details.
**Endpoints:**
- `POST /admin/impersonate/{user}` — start impersonation (requires `role:super_admin` + MFA)
- `POST /admin/stop-impersonation` — stop impersonation (requires `auth:sanctum` only — admin calls without `X-Impersonate-User` header)
- `GET /admin/impersonate/status` — check active session (requires `role:super_admin`)
- `POST /admin/impersonate/send-mfa-code` — send email verification code to admin (requires `role:super_admin`)
#### Start Request
```json
{
"reason": "Investigating user issue with shift assignments",
"mfa_code": "123456",
"mfa_method": "totp"
}
```
| Field | Type | Rules |
|-------|------|-------|
| `reason` | string | required, min:5, max:500 |
| `mfa_code` | string | required |
| `mfa_method` | string | required, in: `totp`, `email`, `backup_code` |
#### Start Response
```json
{
"data": {
"token": "1|abc123...",
"user": { "...AdminUserResource..." },
"admin_id": "01JXYZ..."
"session": {
"id": "01JXYZ...",
"admin_id": "01JXYZ...",
"target_user_id": "01JXYZ...",
"reason": "Investigating user issue",
"mfa_method": "totp",
"started_at": "2026-04-16T12:00:00+00:00",
"expires_at": "2026-04-16T13:00:00+00:00",
"ended_at": null,
"end_reason": null,
"actions_count": 0
},
"user": { "...AdminUserResource..." }
}
}
```
@@ -951,12 +983,33 @@ Base path: `/api/v1/admin/`
}
```
#### Status Response
```json
{
"data": {
"active": true,
"session": { "...ImpersonationSessionResource..." }
}
}
```
#### Impersonation Header
During an active session, the frontend sends `X-Impersonate-User: {target_user_id}` on every request. The `HandleImpersonation` middleware validates the header against the cached session and swaps auth context.
#### Business Rules
- Admin must have MFA enabled (403 if not)
- Cannot impersonate another super_admin (403)
- Impersonation token has name `impersonation-by-{admin_id}`
- Admin ID is cached for 4 hours at key `impersonation:{token_id}`
- Activity log records both start (`admin.impersonation.started`) and stop (`admin.impersonation.stopped`)
- Cannot nest impersonation sessions (403)
- Cannot impersonate a user already being impersonated (403)
- MFA code verified against admin's own MFA (TOTP, email, or backup code)
- Sessions expire after 60 minutes (sliding TTL, extended on each request)
- IP pinning: session terminated if admin's IP changes
- Sensitive routes blocked during impersonation (auth/*, me/*, admin/impersonate/*)
- All activity during session tagged with `impersonated_by` in properties
- Immutable audit trail in `impersonation_sessions` table
## Email Settings (org admin)