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

@@ -279,3 +279,124 @@ Platform admins (`super_admin`) can force-disable MFA for any user via `POST /ad
| `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 |
---
## 10. Impersonation
### 10.1 Overview
Platform admins (`super_admin`) can impersonate other users to investigate issues. The system uses a **header-based approach** — the admin's own httpOnly cookie session is never modified. Instead, the frontend sends an `X-Impersonate-User` header, and a middleware swaps the auth context per-request.
### 10.2 Security Controls
| Control | Implementation |
|---------|---------------|
| **MFA at start** | Admin must provide a valid TOTP, email, or backup code to begin impersonation |
| **Admin MFA required** | Admin's MFA must be enabled (`mfa_enabled = true`) |
| **No super_admin targets** | Cannot impersonate another super_admin |
| **No nesting** | Cannot start a second impersonation while one is active |
| **No double-impersonation** | Cannot impersonate a user already being impersonated by another admin |
| **IP pinning** | If the admin's IP changes mid-session, the session is terminated |
| **Sensitive route blocking** | Auth, MFA, password, email, profile, and impersonation routes are blocked during impersonation |
| **Sliding TTL** | Sessions auto-expire after 60 minutes; each request extends the window |
| **Immutable audit** | Every session is recorded in `impersonation_sessions` with no soft deletes |
### 10.3 Flow
```
Admin (with httpOnly cookie) API
│ │
│ POST /admin/impersonate/{user} │
│ { reason, mfa_code, mfa_method } │
│ ──────────────────────────────────────────►│
│ │ ── Verify admin is super_admin
│ │ ── Verify admin has MFA enabled
│ │ ── Verify target is not super_admin
│ │ ── Check no active session (admin or target)
│ │ ── Verify MFA code
│ │ ── Create ImpersonationSession (DB)
│ │ ── Store session ID in cache (60 min TTL)
│ ◄── session + user data ─────────────────│
│ │
│ GET /any-route │
│ Cookie: crewli_app_token=... │
│ X-Impersonate-User: {target_user_id} │
│ ──────────────────────────────────────────►│
│ │ ── CookieBearerToken: inject admin auth
│ │ ── auth:sanctum: validate admin token
│ │ ── HandleImpersonation middleware:
│ │ 1. Read X-Impersonate-User header
│ │ 2. Check sensitive route block list
│ │ 3. Validate cache session (admin+target)
│ │ 4. Check IP matches session
│ │ 5. Extend sliding TTL
│ │ 6. Store impersonator in request attributes
│ │ 7. Swap auth: setUser(targetUser)
│ │ 8. Increment actions_count
│ │ ── Controller sees targetUser as auth user
│ ◄── response (as target user) ───────────│
│ │
│ POST /admin/stop-impersonation │
│ (NO X-Impersonate-User header) │
│ ──────────────────────────────────────────►│
│ │ ── Admin auth from cookie
│ │ ── End session (DB + cache)
│ ◄── admin user data ─────────────────────│
```
### 10.4 Blocked Routes During Impersonation
The `HandleImpersonation` middleware blocks these route prefixes when the `X-Impersonate-User` header is present:
- `auth/password` — password management
- `auth/logout` — would affect admin's session
- `auth/mfa` — MFA setup/verify/status
- `auth/trusted-devices` — device trust management
- `me/profile` — profile updates
- `me/change-password` — password changes
- `me/change-email` — email changes
- `admin/impersonate` — no nesting via API
- `verify-email-change` — email verification
### 10.5 Activity Log Integration
During an active impersonation session, `AppServiceProvider` hooks into `Activity::saving()` and automatically adds:
- `impersonated_by.user_id` — the admin's user ID
- `impersonated_by.name` — the admin's full name
- `impersonated_by.email` — the admin's email
- `impersonation_session_id` — the session ID
This applies to **all** activity log entries, not just impersonation-specific events.
### 10.6 Database Table
**`impersonation_sessions`** — immutable audit table (no soft deletes)
| Column | Type | Notes |
|--------|------|-------|
| `id` | ULID | PK |
| `admin_id` | ULID FK | → users |
| `target_user_id` | ULID FK | → users |
| `reason` | string | Admin-provided reason for impersonation |
| `mfa_method` | string(20) | Which MFA method was used to verify |
| `ip_address` | string(45) | Admin's IP at session start |
| `user_agent` | text nullable | Admin's user agent |
| `started_at` | timestamp | Session start time |
| `ended_at` | timestamp nullable | NULL = still active |
| `expires_at` | timestamp | Auto-expiry time (sliding, 60 min) |
| `end_reason` | string(50) nullable | manual, expired, ip_changed, admin_kill_all |
| `actions_count` | unsigned int | Number of API requests made during session |
**Indexes:** `(admin_id, ended_at)`, `(target_user_id, ended_at)`, `(started_at)`
### 10.7 Key Files
| File | Purpose |
|------|---------|
| `app/Services/ImpersonationService.php` | Session lifecycle, MFA verification, cache management |
| `app/Http/Middleware/HandleImpersonation.php` | Per-request header validation, auth swap, route blocking |
| `app/Http/Controllers/Api/V1/Admin/AdminImpersonationController.php` | Start, stop, status, send-mfa-code endpoints |
| `app/Http/Requests/Admin/StartImpersonationRequest.php` | Validation for start request |
| `app/Models/ImpersonationSession.php` | Eloquent model with `HasUlids`, `scopeActive()` |
| `app/Http/Resources/Admin/ImpersonationSessionResource.php` | API resource for session data |