Appearance
Impersonation System
Overview
Impersonation allows privileged users (admins with impersonate_access or tech_access on their role) to temporarily assume the identity of another user for debugging and support purposes.
An impersonation session issues a short-lived JWT that identifies both the impersonating admin and the target user. All actions taken during impersonation are recorded in the odp_activity audit log with an impersonated_by field.
Access Control
A user can start impersonation only if their role has either:
tech_access = true(superadmin), ORimpersonate_access = true(explicit impersonation privilege)
Hard Deny Rules
The service applies the following hard checks before creating a session:
| Check | Description |
|---|---|
| Admin is already impersonating | Cannot nest impersonation sessions |
| Target is same as admin | Cannot impersonate yourself |
| Target user is not active | Target must have status = 'active' |
Target has tech_access | Cannot impersonate superadmins |
Target has impersonate_access | Cannot impersonate other impersonation-capable users |
| Max concurrent sessions reached | Default: 3 (configurable via MAX_IMPERSONATION_SESSIONS) |
Endpoints
POST /auth/impersonate
Start an impersonation session.
Auth required: Yes (must have impersonate_access or tech_access)
Request Body:
json
{
"user_id": "uuid-of-target-user",
"reason": "Customer support ticket #12345"
}| Field | Type | Required | Description |
|---|---|---|---|
user_id | string (UUID) | Yes | Target user to impersonate |
reason | string | No | Audit trail note |
Response:
json
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires": "2024-01-01T12:15:00.000Z",
"impersonation_session_id": "uuid-of-session"
}
}Business Logic:
- Validates admin accountability (must be authenticated)
- Checks admin is not already impersonating
- Loads admin user + role to verify
impersonate_accessortech_access - Applies all hard deny checks against target user
- Checks max concurrent active sessions for the admin
- Generates
sessionId(UUID) andsessionToken(random bytes) - Creates impersonation JWT via
createImpersonationToken(admin + target in payload) - Inserts record into
odp_impersonation_sessions - Writes
impersonation.startedtoodp_activity
POST /auth/impersonate/stop
End the current impersonation session.
Auth required: Yes (must be in an active impersonation session — is_impersonating = true)
Request Body: None required
Response:
json
{
"data": {
"ended_at": "2024-01-01T12:10:00.000Z"
}
}Business Logic:
- Checks
accountability.is_impersonating && accountability.impersonation_session - Calls
stopImpersonation(sessionId, 'manual') - Sets
ended_at = now()andend_reason = 'manual' - Writes
impersonation.stoppedtoodp_activity
DELETE /auth/impersonate/sessions/:id
Revoke a specific impersonation session (admin/privileged only).
Auth required: Yes (impersonate_access or tech_access)
URL Parameters:
id— impersonation session UUID
Response: 204 No Content
Business Logic:
- Sets
ended_at = now()andend_reason = 'revoked'
GET /auth/impersonate/sessions
List impersonation sessions.
Auth required: Yes (impersonate_access or tech_access)
Query Parameters:
| Parameter | Description |
|---|---|
admin_id | Filter by admin user UUID |
user_id | Filter by target user UUID |
active_only | true to return only sessions with no ended_at and not expired |
limit | Page size |
offset | Page offset |
Response:
json
{
"data": [
{
"id": "session-uuid",
"admin_user": "admin-uuid",
"target_user": "target-uuid",
"token": "...",
"expires": "2024-01-01T13:00:00.000Z",
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"reason": "Support ticket #123",
"ended_at": null,
"end_reason": null,
"created_at": "2024-01-01T12:00:00.000Z"
}
]
}JWT Structure
The impersonation token is a standard JWT with additional claims:
json
{
"sub": "<target-user-id>",
"admin_sub": "<admin-user-id>",
"impersonation_session": "<session-id>",
"is_impersonating": true,
"iat": 1704067200,
"exp": 1704070800
}The is_impersonating flag is checked throughout the system to:
- Prevent nested impersonation
- Tag audit log entries with
impersonated_by = admin_user_id
Audit Trail
Every impersonation start/stop writes to odp_activity:
action | Description |
|---|---|
impersonation.started | Session created; item = session_id, user = admin_id |
impersonation.stopped | Session ended; item = session_id, user = admin_id |
Actions performed while impersonating use the target user's identity but include impersonated_by = admin_user_id in the activity record.
Cleanup
The ImpersonationService.cleanupExpiredSessions() method marks all expired sessions (where expires < now() and ended_at IS NULL) as ended with end_reason = 'expired'. This should be called periodically via a scheduler.
Data Model
odp_impersonation_sessions
| Column | Type | Description |
|---|---|---|
id | UUID PK | Session identifier |
admin_user | UUID FK → odp_users | User performing impersonation |
target_user | UUID FK → odp_users | User being impersonated |
token | string(64) | Session lookup token |
expires | timestamp | Session expiry (from JWT expiry) |
ip | string(255) | IP of request that started impersonation |
user_agent | text | User-agent of request |
reason | text | Optional audit note |
ended_at | timestamp | When session ended (null if active) |
end_reason | string(50) | manual, expired, or revoked |
created_at | timestamp |
Configuration
| Variable | Default | Description |
|---|---|---|
MAX_IMPERSONATION_SESSIONS | 3 | Maximum concurrent active sessions per admin |
Security Notes
- Impersonation tokens are short-lived (defined by
createImpersonationToken— typically 1 hour) - Impersonation cannot be used to impersonate users with
tech_accessorimpersonate_access - Sessions are immutable once created — they can only be ended, not modified
- All activity is double-logged: at session start/stop AND at each action taken during impersonation