|
48 | 48 | - [Performing a delegation exchange without a session](#performing-a-delegation-exchange-without-a-session) |
49 | 49 | - [Using actor tokens for delegation](#using-actor-tokens-for-delegation) |
50 | 50 | - [Passing `StoreOptions`](#passing-storeoptions-6) |
| 51 | +- [Impersonation via Session Transfer](#impersonation-via-session-transfer) |
| 52 | + - [Initiator: requesting a Session Transfer Token and redirecting](#initiator-requesting-a-session-transfer-token-and-redirecting) |
| 53 | + - [Target: redeeming the Session Transfer Token](#target-redeeming-the-session-transfer-token) |
| 54 | + - [Reading the `act` claim on the impersonation session](#reading-the-act-claim-on-the-impersonation-session) |
51 | 55 | - [Retrieving the logged-in User](#retrieving-the-logged-in-user) |
52 | 56 | - [Passing `StoreOptions`](#passing-storeoptions-7) |
53 | 57 | - [Retrieving the Session Data](#retrieving-the-session-data) |
@@ -1159,6 +1163,111 @@ const tokenResponse = await serverClient.customTokenExchange({ subjectToken, sub |
1159 | 1163 |
|
1160 | 1164 | Read more above in [Configuring the Store](#configuring-the-store) |
1161 | 1165 |
|
| 1166 | +## Impersonation via Session Transfer |
| 1167 | +
|
| 1168 | +Custom Token Exchange Impersonation via Session Transfer lets a support/admin application log an agent **into a target web application as a customer** — for example, so a support engineer can reproduce a customer's exact experience without ever knowing their password. It builds on Custom Token Exchange and involves two roles and two applications: |
| 1169 | +
|
| 1170 | +- **Initiator** — your support/admin app (this SDK). It requests a short-lived, single-use **Session Transfer Token (STT)** and redirects the agent's browser to the target app's login URL carrying the STT. |
| 1171 | +- **Target** — the customer's own web app. Its login route forwards the STT to `/authorize`, where Auth0 redeems it and establishes an ephemeral, device-bound session **as the customer**, recording the agent in the `act` (actor) claim. |
| 1172 | +
|
| 1173 | +The STT is **opaque, single-use, and short-lived (~60s)**. The SDK requests it, surfaces it, and helps you build the redirect — it never decodes, validates, caches, or persists it. |
| 1174 | +
|
| 1175 | +> [!IMPORTANT] |
| 1176 | +> This is a two-client, two-role flow with tenant prerequisites configured out of band (via the Management API / Dashboard): |
| 1177 | +> |
| 1178 | +> - The feature flag `cte_session_transfer_token` must be enabled on the tenant. |
| 1179 | +> - The **issuing (initiator) client** needs `session_transfer.can_create_session_transfer_token: true` and a Custom Token Exchange profile / Action that calls `setActor`. |
| 1180 | +> - The **redeeming (target) client** needs `session_transfer.delegation.allow_delegated_access: true`, `session_transfer.allowed_authentication_methods` including `"query"`, and `session_transfer.delegation.enforce_device_binding` (`"ip"` by default). |
| 1181 | +> |
| 1182 | +> See the [Custom Token Exchange documentation](https://auth0.com/docs/authenticate/custom-token-exchange) for the full setup. |
| 1183 | +
|
| 1184 | +### Initiator: requesting a Session Transfer Token and redirecting |
| 1185 | +
|
| 1186 | +The agent must be **logged in** to the initiator app: the SDK sources the actor from the agent's current session ID token by default (refreshing it if it has expired). Run your own authorization check — _is this agent allowed to impersonate this customer, right now?_ — before calling the SDK. |
| 1187 | +
|
| 1188 | +```ts |
| 1189 | +// In your support console's "View as customer" route (agent is already logged in): |
| 1190 | +const result = await serverClient.requestSessionTransferToken( |
| 1191 | + { |
| 1192 | + // Your own proof of which customer to impersonate — validated by your Action. |
| 1193 | + // The SDK never produces this; you supply it in whatever form your Action expects. |
| 1194 | + subjectToken, |
| 1195 | + subjectTokenType: 'urn:acme:customer-subject', |
| 1196 | + // Optional: forward custom context to your Action (e.g. an audited reason). |
| 1197 | + extra: { reason: 'Investigating TCK-4821' }, |
| 1198 | + }, |
| 1199 | + storeOptions |
| 1200 | +); |
| 1201 | + |
| 1202 | +// Build the redirect to the TARGET app's login URL (a trusted, app-controlled value). |
| 1203 | +const url = serverClient.buildSessionTransferRedirect('https://app.example.com/auth/login', result); |
| 1204 | + |
| 1205 | +// Hand the URL to your framework's redirect (e.g. Fastify: reply.redirect(url.toString())). |
| 1206 | +``` |
| 1207 | +
|
| 1208 | +By default the actor is the agent session's ID token. To supply the acting party explicitly, pass `actor`: |
| 1209 | +
|
| 1210 | +```ts |
| 1211 | +const result = await serverClient.requestSessionTransferToken( |
| 1212 | + { |
| 1213 | + subjectToken, |
| 1214 | + subjectTokenType: 'urn:acme:customer-subject', |
| 1215 | + actor: { token: agentIdToken }, // type defaults to the ID token URN |
| 1216 | + }, |
| 1217 | + storeOptions |
| 1218 | +); |
| 1219 | +``` |
| 1220 | +
|
| 1221 | +If the customer belongs to an organization, forward it on the redirect so it reaches the target's `/authorize`: |
| 1222 | +
|
| 1223 | +```ts |
| 1224 | +const url = serverClient.buildSessionTransferRedirect('https://app.example.com/auth/login', result, { |
| 1225 | + organization: 'org_globex', |
| 1226 | +}); |
| 1227 | +``` |
| 1228 | +
|
| 1229 | +> [!IMPORTANT] |
| 1230 | +> An **actor is mandatory** for an STT — that is what makes this auditable impersonation ("X acting as Y") rather than a silent takeover. If no explicit `actor` is passed and no usable session ID token can be resolved (no logged-in agent, or an expired ID token with no refresh token), the SDK throws a `TokenExchangeError` with code `actor_unavailable` **before any network call**. The agent's session ID token must also be unexpired; the SDK refreshes it automatically when a refresh token is available. |
| 1231 | +
|
| 1232 | +> [!NOTE] |
| 1233 | +> `buildSessionTransferRedirect` attaches a single-use credential to the URL, so `targetLoginUrl` **must be a trusted, app-controlled value** and use `https` (`http` is allowed only for the loopback hosts `localhost`, `127.0.0.1`, and `[::1]`). Never derive it from untrusted input such as a `returnTo` parameter, or the token could leak to an attacker-controlled host. |
| 1234 | +
|
| 1235 | +The exchange is **stateless** — the STT is never written to the session or state store. Do not cache or persist it; hand it straight to the redirect and discard it. |
| 1236 | +
|
| 1237 | +### Target: redeeming the Session Transfer Token |
| 1238 | +
|
| 1239 | +On the target app, the STT is redeemed as part of a **standard interactive login** — you just forward `session_transfer_token` (and `organization`, when present) to `/authorize` via `startInteractiveLogin`'s `authorizationParams`: |
| 1240 | +
|
| 1241 | +```ts |
| 1242 | +// In the target app's login route, reading session_transfer_token from the query string: |
| 1243 | +const url = await serverClient.startInteractiveLogin( |
| 1244 | + { |
| 1245 | + authorizationParams: { |
| 1246 | + session_transfer_token: sessionTransferToken, |
| 1247 | + organization, // only when the STT was issued in an organization context |
| 1248 | + }, |
| 1249 | + }, |
| 1250 | + storeOptions |
| 1251 | +); |
| 1252 | + |
| 1253 | +// Redirect the browser to `url`; the callback completes with a standard authorization-code login. |
| 1254 | +``` |
| 1255 | +
|
| 1256 | +> [!NOTE] |
| 1257 | +> The `session_transfer_token` is redeemed as a **query** parameter, so the redeeming client's `session_transfer.allowed_authentication_methods` must include `"query"`. The established session is short-lived (hard-capped at 2 hours) and **cannot mint a refresh token** — to continue, re-run the whole flow. If the target needs `online_access`, set it through the SDK's `scope` configuration. |
| 1258 | +
|
| 1259 | +> [!NOTE] |
| 1260 | +> Because the STT travels as a query parameter, it can land in places that log or retain full URLs — web-server access logs, proxy/CDN logs, and the browser's history. This is inherent to the redemption mechanism and is mitigated by the token being **single-use and short-lived (~60s)** and, when configured, **device-bound** (`enforce_device_binding`): a leaked STT is worthless once redeemed or expired. Even so, avoid logging redemption URLs verbatim, and never persist or forward the STT beyond the immediate redirect. |
| 1261 | +
|
| 1262 | +### Reading the `act` claim on the impersonation session |
| 1263 | +
|
| 1264 | +Once the target session is established, the acting agent is available as the `act` claim on the session user — read it through the existing session surface to drive UI such as an impersonation banner: |
| 1265 | +
|
| 1266 | +```ts |
| 1267 | +const session = await serverClient.getSession(storeOptions); |
| 1268 | +const actor = session?.user?.act; // { sub: 'support-agent-007', ... } when impersonated |
| 1269 | +``` |
| 1270 | +
|
1162 | 1271 | ## Passwordless Authentication |
1163 | 1272 |
|
1164 | 1273 | The server client exposes passwordless as session-aware methods around the Authentication API. The surface mirrors `@auth0/nextjs-auth0` (`passwordless.start()` / `passwordless.verify()`): `startPasswordless` sends the code or link; `completePasswordless` exchanges the one-time code and persists the resulting session to the configured state store (no redirect, no PKCE). Both methods are discriminated on `connection` (`'email' | 'sms'`). |
|
0 commit comments