Skip to content

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), OR
  • impersonate_access = true (explicit impersonation privilege)

Hard Deny Rules

The service applies the following hard checks before creating a session:

CheckDescription
Admin is already impersonatingCannot nest impersonation sessions
Target is same as adminCannot impersonate yourself
Target user is not activeTarget must have status = 'active'
Target has tech_accessCannot impersonate superadmins
Target has impersonate_accessCannot impersonate other impersonation-capable users
Max concurrent sessions reachedDefault: 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"
}
FieldTypeRequiredDescription
user_idstring (UUID)YesTarget user to impersonate
reasonstringNoAudit trail note

Response:

json
{
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires": "2024-01-01T12:15:00.000Z",
    "impersonation_session_id": "uuid-of-session"
  }
}

Business Logic:

  1. Validates admin accountability (must be authenticated)
  2. Checks admin is not already impersonating
  3. Loads admin user + role to verify impersonate_access or tech_access
  4. Applies all hard deny checks against target user
  5. Checks max concurrent active sessions for the admin
  6. Generates sessionId (UUID) and sessionToken (random bytes)
  7. Creates impersonation JWT via createImpersonationToken (admin + target in payload)
  8. Inserts record into odp_impersonation_sessions
  9. Writes impersonation.started to odp_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:

  1. Checks accountability.is_impersonating && accountability.impersonation_session
  2. Calls stopImpersonation(sessionId, 'manual')
  3. Sets ended_at = now() and end_reason = 'manual'
  4. Writes impersonation.stopped to odp_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() and end_reason = 'revoked'

GET /auth/impersonate/sessions

List impersonation sessions.

Auth required: Yes (impersonate_access or tech_access)

Query Parameters:

ParameterDescription
admin_idFilter by admin user UUID
user_idFilter by target user UUID
active_onlytrue to return only sessions with no ended_at and not expired
limitPage size
offsetPage 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:

actionDescription
impersonation.startedSession created; item = session_id, user = admin_id
impersonation.stoppedSession 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

ColumnTypeDescription
idUUID PKSession identifier
admin_userUUID FK → odp_usersUser performing impersonation
target_userUUID FK → odp_usersUser being impersonated
tokenstring(64)Session lookup token
expirestimestampSession expiry (from JWT expiry)
ipstring(255)IP of request that started impersonation
user_agenttextUser-agent of request
reasontextOptional audit note
ended_attimestampWhen session ended (null if active)
end_reasonstring(50)manual, expired, or revoked
created_attimestamp

Configuration

VariableDefaultDescription
MAX_IMPERSONATION_SESSIONS3Maximum 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_access or impersonate_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

ODP Internal API Documentation