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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user