Support impersonation threat model#
Scope: support access as implemented in internal/impersonation, the routes in
internal/httpapi/admin.go and internal/httpapi/selfservice.go, introspection in
internal/oauth/introspect.go, the decision API in internal/httpapi/decision.go,
and the downstream guard verifier.SupportGuard in proudauth-sdk-go/verifier/introspect.go.
Requirements: docs/plan.md §16 (IMP-001..IMP-024). Design:
ADR 0014.
Assets#
- The customer's data and actions in downstream applications.
- The customer's identity and credentials (must never change under support access).
- Accountability: the record that a named operator, not the customer, acted.
- The support token (
pa_it_...) and handoff code (pa_hc_...).
Actors#
| Actor | Trust |
|---|---|
| Support operator | Authenticated administrator with support.impersonation.start in the target organization; may be malicious or compromised |
| Approver | Second operator (support.impersonation.approve), organization owner (org.support_access.approve), or the customer, depending on policy |
| Customer (target) | Can view, revoke and (if the realm allows) opt out |
| Support backend | Confidential client that receives the token |
| Destination application backend | Confidential client that redeems handoff codes |
| Resource server | Registered introspector for the audience; must implement the contract |
| Attacker | Holds a stolen admin token, support token, handoff code or browser URL |
Flow and checks#
- Enablement (IMP-001).
impersonation.enabled(deployment), realm policyimpersonation.enabled, and at least one active resource withimpersonation_enabledandverification_mode: strict_online. Checked at request, approval and issuance (Service.enabled), and again on every use of a live grant (ValidateGrant, step 5). - Request
POST /impersonation-requests(admin audience).checkActor:user_accesstoken on a live session of the same user, MFA withinsecurity.admin_mfa_max_ageand not recovery-restricted, user active, permissionsupport.impersonation.startin the target organization. Reason length ≥impersonation.min_reason_length(≤ 1000); ticket when the deployment or realm requires it. Target: not self, active, notprotected, no privileged realm role, active member of an active organization, not holdingorg.roles.assign,org.ownership.transferororg.members.writethere, not opted out (when the realm permits opt-out), organization not opted out. Client: active, confidential, in the resource'ssupport_client_idswhen set. Scopes: subset of the resource'sallowed_scopes(pluswrite_scopesonly inallowlisted_write). Request expires after 15 minutes. - Approval (
approvalfrom realm policy, overridden by organization policy;allowlisted_writeforcessecond_operatorwhen policy saysnone). Requester cannot approve. Second operator and organization owner go throughcheckActorwith their permission;customerrequires the target user, who is emailed at request time and sees the request inGET /me/impersonation-requests(IMP-022,TestCustomerApprovedSupportAccess).POST /impersonation-requests/{id}/denyrefuses a pending or approved request: the requester can withdraw it, and whoever may approve under the request's policy can deny it; other callers get404. - Issue
POST /impersonation-requests/{id}/issueby the requester only. Re-runscheckActor, organization and membership liveness, client and resource eligibility, recomputes scopes; inserts the grant, the opaque token, thesupport.impersonation.startedaudit event, the outbox event and (ifnotify_customer) the notification in one transaction; the token is returned only after commit (IMP-020). - Use. The resource introspects every request. Introspection returns
active: trueonly if the token, its resource and client are active andValidateGrantpasses: grant active and unexpired; actor and target users active; target membership active; organization active; actor session live; actor still holdssupport.impersonation.startin the organization, within any capability conditions on the operator's role assignment (organization_ids,subject_categories,support_modes,support_scopes; IMP-002, IMP-007,TestSupportCapabilityProfile); the deployment and realm switches are still on; the resource is still active, strict-online and opted in; and neither the organization nor (where the realm permits it) the customer has opted out. The response carriesact,support_mode,impersonation_grant_idandtoken_kind: impersonation_access; each active result is audited assupport.token.introspected. - Handoff (optional).
POST /impersonation-grants/{id}/handoffsreturns a single-use code valid ≤impersonation.handoff_ttl(30 s, capped at the grant expiry), bound to a confidential destination client allowed by the resource. The destination backend redeems it with client authentication atPOST /impersonation-handoffs/redeemfor its own support token on the same grant. - End. Expiry (≤ 5 minutes), revocation by the actor, the target
(
/me/impersonation-grants/{id}/revoke), a holder ofsupport.impersonation.revoke, or the customer opting out; revocation also revokes the grant's tokens and unconsumed handoff codes.
Threats#
| # | Threat | Mitigation | Tests |
|---|---|---|---|
| I-1 | Operator without authority starts support access | Dedicated permission scoped to the organization; admin audience; live MFA session | TEST-040 |
| I-2 | Stolen admin access token used to start access | Session must be live and MFA recent; token kind must be user_access (no machine or exchanged tokens) | TEST-040, TEST-039 |
| I-3 | Impersonating a more privileged user to escalate | Protected users, privileged realm roles and organization administrators excluded | TEST-041 |
| I-4 | Support token used against the IdP (change password, MFA, create keys, start nested support) | pa_it_ tokens are rejected by every IdP API (403 impersonation_restricted); the support.token.rejected audit event names the operator as actor, the customer as subject, the support grant, the organization and the route when the token is known (IMP-006); token exchange refuses them; no refresh token issued | TEST-042, TEST-043 |
| I-5 | Support token used for protected business operations | SupportGuard denies the protected operations (identity and money operations, ownership transfer, secrets, delete, purge: Go verifier.ProtectedOperationList() plus the guard's own ProtectedOperations) wherever their segments occur in the operation name, treats any operation with a write-verb segment as a write, and denies writes outside WriteOperations; the decision API applies the same rule (authorization.ImpersonationForbiddenActions, authorization.WriteSegments), denies non-read actions in read-only mode and, in allowlisted_write mode, writes not listed in the resource's impersonation_policy.write_actions (SUPPORT_ACCESS_WRITE_NOT_ALLOWLISTED) | TEST-042; all five SDKs and the server's classification run tests/fixtures/sdk-contracts/support-operations/cases.json |
| I-6 | Token outlives the reason for access | ≤ 5 min (config validation caps impersonation.max_ttl), per-request online validation of grant, actor, target, membership, organization, actor session, actor permission | TEST-043, TEST-044 |
| I-7 | Operator acting under the customer's name | act in introspection; grant stores actor and actor MFA time; IdP audit events carry actor and grant ID; SupportGuard records the actor before access | TEST-045 |
| I-8 | Support token leaked through a browser URL | Handoff codes are short, single-use, destination-bound and redeemed by an authenticated backend | TEST-046 |
| I-9 | Access granted although evidence was not recorded | Grant, token and audit event in one transaction; SupportGuard denies when its auditor fails | TEST-047 |
| I-10 | Customer unaware or unable to stop access | /me/impersonation-grants (list, revoke), opt-out (PUT /me/support-access) that revokes active grants, optional email notification | TEST-071 |
| I-11 | Self-approval or collusion | Requester cannot approve; operator and organization-owner approvers need their own permission and recent MFA (customer approval needs neither, see residual risk 10); allowlisted_write always needs an approver (a second operator when policy says none) | TestCustomerApprovedSupportAccess; other approval paths by code review |
| I-12 | Target's historical MFA reused as support assurance | Support tokens carry no acr/amr/auth_time of the target; actor MFA time stored separately | — |
| I-13 | Support token accepted by a non-participating resource | Opaque tokens are only usable through introspection, which is restricted to registered introspectors of the token's audience | TEST-037 |
| I-14 | Feature enabled with no resource ready | Request refused until a strict-online resource opts in | IMP-001 subtest |
Residual risks and gaps#
These are known differences from §16; treat them as limitations.
- No intersection with the target's current permissions at the IdP (IMP-007,
IMP-017). The scope ceiling is the resource's support policy ∩ requested
scopes (∩ the operator's
support_scopescapability when set); the target's permissions are not part of it. A resource gets the target's current permissions only by calling the decision API (POST /authorization/check) with the support token, which evaluates the target's effective permissions, or by checking its own data. A reduction of the target's permissions therefore affects support access only where the resource does so. - Disabling ends live grants, but realm state is not re-checked.
ValidateGrantre-checks the deployment switch, the realm switch, the resource'simpersonation_enabledflag and verification mode, the organization'ssupport_access_opt_outpolicy and the customer's opt-out on every introspection, so turning any of them off makes live support tokens inactive immediately (no test exercises this yet). Realm state (for example a suspended realm) is not part of that check, and there is no bulk-revoke endpoint; the grants stayactivein the inventory until they expire. - Denial evidence is recorded after rollback; self-targeting is not.
RequestAccesswritessupport.impersonation.deniedandRedeemHandoffwritessupport.handoff.rejectedafter the failed transaction rolls back, so the evidence survives (and feeds theimpersonation_deniedsecurity signal). An operator targeting themselves is refused without an audit event. No test asserts these denial events. - Protected actions and write allowlist (resolved).
authorization.ImpersonationForbiddenActionsequals the SDKs' protected operations (Goverifier.ProtectedOperationList(), Node.jsPROTECTED_OPERATIONS; includingsecretandpurge), andIsSupportForbiddenandIsWriteActionuse the SDK rule: segments split on.,:and/, matched as a contiguous run with simple plurals, and any write-verb segment makes an action a write whatever its suffix (IMP-011, IMP-013). The decision API denies protected actions for every support token in either mode, and allows anallowlisted_writegrant only the writes the resource lists inimpersonation_policy.write_actions. The server's classification runs the shared fixture too (TestSupportClassificationMatchesSDKTable); the decision API'swrite_actionscheck has no end-to-end test. The Go SDK keeps the lists unexported and hands out copies (verifier.ProtectedOperationList(),verifier.WriteSegmentList(),verifier.DefaultOnlineKindList()), and the Node.js SDK freezes them, so code in the same process cannot weaken them. In Node.js, a list passed as the second argument ofisProtectedOperation(op, list)replaces the defaults (SupportGuardmerges its own list with them). - Target revocation is unconditional. The target can always revoke; there is no policy to disable it (IMP-016 says "where policy permits").
- Resource enforcement is the resource's responsibility. A resource that
introspects but ignores
token_kind,support_modeandactis not prevented from treating the token as the customer's own. UseSupportGuardor an equivalent and callValidate()at startup. - Evidence of one grant (IMP-024): the audit filter
impersonation_grant_id(query and export) also returns the grant's request-phase events (support.impersonation.requestedand.approved, which target the request), andsupport.impersonation.startedcarriesrequest_id. A denied request never becomes a grant, sosupport.impersonation.request_deniedis found by the request ID (target_id), not by this filter. Any request presenting apa_it_-shaped bearer to an IdP API writes asupport.token.rejectedevent, including unknown tokens. - Grant inventory filters are actor, target, organization, state and
ticket_reference(IMP-023); results are cursor-paginated, but the underlying query still returns at most the 500 newest matching grants. - Introspection audit is best-effort.
support.token.introspectedis written outside a transaction and its error is ignored; the authoritative per-request record is the resource'sSupportAuditor. - Customer approval uses the tenant route with an account-audience token; the customer does not need MFA to approve.
- Customer-visible reason only before approval. Requests awaiting the
customer's approval show the reason and ticket in
GET /me/impersonation-requests, and the approval email includes the reason (IMP-022). Issued grants listed in/me/impersonation-grantsstill omit the reason.
Not impersonation: just-in-time elevation#
Just-in-time elevation (AZ-024, elevation-and-access-reviews.md) is sometimes confused with support access. It is different: the elevated person acts as themselves, with a role they were made eligible for, for a bounded time, after an independent approval (always, for privileged roles). Their tokens and audit events name them; no customer's identity is involved, no support token exists and none of the controls above apply. The threats are those of any role assignment (delegation ceilings, separation of duties, self-approval) and are covered in the system threat model. Use support access to act for a customer, and elevation to hold more of your own authority for a while; neither substitutes for the other.