Skip to main content

Impersonate a tenant for support work

Outcome

You see exactly what the customer sees, every action attributed to you in the audit log, with a banner reminding you the session is live.

Prerequisites

  • PLATFORM_ADMIN or PLATFORM_SUPPORT. PLATFORM_READONLY is forbidden — a direct POST /platform/tenants/:id/impersonate returns 403 and writes IMPERSONATION_FORBIDDEN to the audit log.
  • Tenant status must not be offboarding. Offboarding tenants reject impersonation to keep the export-only window clean.
  • JWT_IMPERSONATION_EXPIRY (default 3600 s) governs how long the session lasts. There is no refresh token — when the JWT expires the banner redirects you back to the tenant list.

What an impersonation is — and isn't

IsIsn't
A short-lived JWT with scope=impersonation, your platform_user_id, and the target tenant_idA login as the tenant — there's no refresh token
Recorded as IMPERSONATION_STARTED / IMPERSONATION_ENDED in identity.tenant_auditA way to bypass PLATFORM_READONLY (forbidden, audited)
Persisted in sessionStorage (cleared on tab close)Persisted in localStorage (would survive tab close)
Subject to the audit middleware on every tenant API callBypass for suspended tenants — they still 503

Flow

Steps

  1. Open the tenant page

    Platform Admin → Tenants → <tenant>. Click the Impersonate button in the page header. The button is hidden for PLATFORM_READONLY and disabled when the tenant is offboarding.

  2. Confirm the consequences

    The modal explains: every action you take is attributed to your platform user; the session ends on click or after the JWT TTL; closing the tab silently ends the session client-side (no IMPERSONATION_ENDED row).

  3. Work in tenant context

    After confirm, the page reloads to /. The ImpersonationBanner mounts at the top of the layout and counts down to expiry. Every API call you make during the session shows up in audit.access_log with your platform identity and in identity.tenant_audit if it touches platform-managed surfaces.

  4. End the session

    Click End impersonation in the banner. This posts to /platform/impersonation/end, writes IMPERSONATION_ENDED, and redirects back to the tenant list.

    If you let the timer run out (or close the tab), the session ends but no IMPERSONATION_ENDED row is written. That's expected — the JWT is server-side invalid, but the operator never explicitly closed the session.

Validation

CheckExpected
IMPERSONATION_STARTED row in identity.tenant_auditdetails.email = your platform email
IMPERSONATION_ENDED rowmatches the START's platform_user_id (within JWT TTL)
Platform Admin → Tenants → <tenant> → Impersonation tabshows the session paired with startedAt + endedAt
Tenant-side audit.access_logrows during the window are attributed to your impersonation token

Troubleshooting

SymptomCauseFix
Impersonate button hiddenYou are PLATFORM_READONLYAsk a PLATFORM_ADMIN to act on your behalf, or escalate the role temporarily.
Click → 403 + IMPERSONATION_FORBIDDEN audit rowTenant is offboarding, or RBAC driftCheck identity.tenant.status. If offboarding, the customer is mid-export — flip to active only with a written exception.
Banner shows but no tenant data rendersThe tenant database is unreachableSee Tenant DB unreachable.
Session ends without IMPERSONATION_ENDED rowJWT TTL elapsed or tab closedExpected. To audit "abandoned" sessions per platform user, compare IMPERSONATION_STARTED count vs. IMPERSONATION_ENDED count over a window.
History tab shows endedAt: null for an old sessionThe IMPERSONATION_ENDED row falls outside the page's limitRe-query with a larger limit. Pairing logic matches each START with the next END for the same platform_user_id.

Cross-references

Next

1.3 — Suspend or read-only a tenant