navree
Documentation

Impersonation

Let support staff sign in as a user, with a required reason, guards on risky routes and the impersonator on every audit entry.

On this page

Support often needs to see what a user sees. Impersonation lets an administrator sign in as another user without their password: the requests run as that user, while the token, the request and every audit entry also name the administrator. Risky routes refuse impersonation sessions, and each session ends after an hour. This page assumes AuthModule is set up as described in Authentication, with AccessModule for the permission check (Authorization).

Turning it on

Impersonation is off by default. Turn it on and grant the permission users.impersonate to the roles that may use it:

config/app.toml
src/seed.rs

With enabled = false the routes aren't mounted, and AuthService::impersonate answers AuthError::Disabled. The route checks its permission through the app's route authorizer, so the app needs AccessModule.

Starting a session

POST /api/auth/impersonate/{user} with a reason signs the caller in as the user:

HTTP
JSON

The tokens belong to a new session of the user. Keep the administrator's own tokens aside and use these until the session ends.

ProblemResponse
The caller lacks users.impersonate403 Forbidden
No reason, or a blank one; more than 500 characters400 field error on reason
The caller is impersonating already403 Not allowed while impersonating a user
The caller's own id403 This user cannot be impersonated: yourself
A platform administrator403 This user cannot be impersonated: a platform administrator
A user who holds the impersonation permission403 This user cannot be impersonated: a user who may impersonate others
An unknown user404 Not Found

The last rule stops support agents from impersonating each other, or an administrator above them, to borrow their rights.

What the session looks like

The access token carries an act claim (RFC 8693) with the administrator's id, and no auth_time:

JSON

Requests made with it are the user's: CurrentUser is the user, route authorization uses the user's roles, and data is scoped as theirs. The JWT strategy also puts a navree::contracts::Impersonator in the request, which three things pick up:

  • Actor, an extractor naming both people, for handlers and for a "You are impersonating Ada" banner.
  • The unit of work, which carries the impersonator (Uow::impersonator()).
  • The audit trail, which records the impersonator on every entry of the request (below).
Rust

Actor { user_id, impersonator_id } answers 401 for guests. real_user_id() is the person at the keyboard: the impersonator while impersonating, the user otherwise.

Refreshing keeps the impersonator and the session's end: an impersonation session lasts auth.impersonation.ttl_secs (an hour) from its start, however often it is refreshed. The user sees it in their session list, with impersonator_id set, and can end it.

Guarding risky routes

An administrator acting as a user shouldn't change the user's password, email address or second factor. Put #[not_while_impersonating] on routes like these:

src/modules/billing/controller.rs

An impersonation session gets a 403 with the code impersonation_forbidden:

JSON

On the impl block it applies to every route. It is a guard, not an access declaration, and it needs the auth feature of the navree facade. For hand-written routes, add .guard(navree::auth::not_while_impersonating()).

The auth module's own account-security routes carry it already:

  • PATCH /api/auth/me and PATCH /api/auth/me/password;
  • every /api/auth/2fa/* route except verify;
  • POST /api/auth/confirm and DELETE /api/auth/sessions/{id};
  • POST /api/auth/impersonate/{user}, so sessions don't nest;
  • the passkey registration and removal routes.

#[recent_auth] routes refuse impersonation sessions too, because an impersonation session never signed in (Sessions and step-up). Reading stays possible: GET /api/auth/me and every route without these guards work as for the user.

Ending the session

HTTP

The answer is 204 No Content, and the session's refresh tokens stop working. Its access token works until it expires, so the client drops it and switches back to the administrator's own tokens. A token that isn't an impersonation token gets 400 Not impersonating a user.

The audit trail

Every audit entry written while impersonating names both people. actor_id is the user, as for their own requests, and impersonator_id is the administrator:

JSON
  • Data changes get it from the request's unit of work.
  • Entries recorded during the request, through AuditLog::record or the AuditSink, get it from the task's current unit of work; record_in takes it from the unit of work it is given.
  • The auth module's own entries carry it too: auth.refresh, auth.logout and the rest.

GET /api/audit?impersonator_id=<id> lists everything an administrator did as other users. See Audit log.

The sessions themselves are recorded as security events:

ActionKindActorSubjectMetadata
auth.impersonation_startedsecurityThe administratorThe userreason, session_id
auth.impersonation_endedsecurityThe administratorThe usersession_id

In code

AuthService methodWhat it does
impersonate(impersonator_id, impersonator_claims, target_id, reason, &meta)Starts a session as target_id: the user and a TokenPair
stop_impersonating(&claims, &meta)Ends the impersonation session of claims

impersonator_claims is the caller's AccessClaims, when there is one: an impersonation session can't start another. The route checks the permission; the method checks everything else in the table above.

Testing

Give a test user the permission, start a session over HTTP and use its token:

Rust

builder stands for your test setup (Authentication).

Limits

  • The front end keeps the administrator's tokens. Ending an impersonation session doesn't return the administrator's own tokens.
  • One session at a time. An impersonation session can't impersonate further.
  • Guards are opt-in for your routes. Only routes with #[not_while_impersonating] (or #[recent_auth]) refuse impersonation sessions; review your own risky routes.