feat(sessionTransferToken): add CTE impersonation via Session Transfer Token#850
feat(sessionTransferToken): add CTE impersonation via Session Transfer Token#850cschetan77 wants to merge 15 commits into
Conversation
|
|
||
| let redirectUrl; | ||
| try { | ||
| redirectUrl = new URL(targetLoginUrl); |
There was a problem hiding this comment.
new URL(targetLoginUrl) accepts any scheme, so an http: (or javascript:/data:) target parses fine and the STT gets appended as a query param. The JSDoc already says to use https: in production, and since this helper is the one place that hardening can live, an http:/https: allowlist (with a localhost exception for dev) would make that guidance actually hold.
There was a problem hiding this comment.
Done. buildSessionTransferRedirect now rejects any targetLoginUrl that isn't https:, with an exception for http: on localhost, 127.0.0.1, and [::1] to support local development. Any other scheme (http: on a non-loopback host, ftp:, javascript:, data:, etc.) throws a TypeError. Aligns with the same check in auth0-server-js.
Five tests added covering the rejection cases and all three loopback variants.
…SessionTransferRedirect
Adds CTE Phase 2 — Impersonation via Session Transfer — to the SDK.
- req.oidc.requestSessionTransferToken({ subject_token, subject_token_type, ... })
performs a token exchange against urn:{domain}:session_transfer, automatically
sourcing the actor from the agent's session id_token (refreshed if expired).
Returns a SessionTransferTokenResult. Never writes the STT to the session.
- req.oidc.buildSessionTransferRedirect(targetLoginUrl, result, opts?)
builds the redirect URL with session_transfer_token (and optional organization)
as query params. Pure helper — no network call, no session writes.
New TypeScript types: SessionTransferTokenResult, SessionTransferTokenOptions,
SessionTransferRedirectOptions. New error codes surfaced via existing createError:
actor_unavailable (client-side), setactor_required and session_transfer_disabled
(server 400s).
…alidation and types - Validate issued_token_type matches the STT URN before accepting a result — prevents accidentally passing a regular customTokenExchange response - Add typeof check on session_transfer_token to catch non-string values - Switch all buildSessionTransferRedirect throws from createError to TypeError — this is a pure synchronous helper, not an HTTP operation - Use global URL (WHATWG) instead of url.URL in both STT methods - Tighten SessionTransferTokenOptions docs: remove SDK-internal wording, keep general - Add https recommendation to buildSessionTransferRedirect doc comment
…rror messages and comments
…ry check, empty issued_token_type fallback, extra params over reason
…ken section to EXAMPLES.md
…o prevent flaky CI failures
7e98028 to
5d5ff81
Compare
…clarations SESSION_TRANSFER_TOKEN_IDENTIFIER and ID_TOKEN_IDENTIFIER were exported from index.d.ts but never from the runtime entry, so importing them would yield undefined at runtime. Both were tagged @internal and have no consumer use case — removed from the type declarations entirely.
…s no new id_token After a successful refresh, re-check the id_token expiry before using it as the actor_token. If the refresh grant returned no new id_token the old expired token is kept in the session and would be rejected server-side on the exp check. Adds a test covering this path.
…t at redirect Check that the AS response carries the STT URN immediately after the exchange succeeds. A missing or unexpected issued_token_type now throws 500 (valid upstream response, unexpected payload) rather than silently returning a result that buildSessionTransferRedirect would reject later. Refactored the try/catch to only wrap the network call so the validation sits clearly outside it.
Reject any targetLoginUrl that is not https, except http on localhost, 127.0.0.1, and [::1] for local development. The STT is a single-use credential — appending it to an http or non-http/https URL would expose it over an insecure or untrusted channel. Aligns with auth0-server-js.
…meric exp A missing or malformed exp claim (undefined, NaN, string) evaluated as false in the <= comparison, treating the token as unexpired and forwarding it as the actor token. Added a typeof + isFinite guard so any non-numeric exp is treated as expired, triggering a refresh or actor_unavailable.
If the stored id_token is present but undecodable, ts.claims() returns
undefined and destructuring exp would throw a TypeError (500) instead of
staying within the documented actor_unavailable 400 path. The || {}
fallback makes exp undefined, which isIdTokenExpired now treats as
expired, triggering refresh or actor_unavailable as expected.
…ken_type branching note Two developer-facing gaps in the EXAMPLES.md guide: - Explicit actor_token must be unexpired and RS256/PS256 signed; HS256 and expired tokens are rejected server-side. - Branch on issued_token_type, not token_type — token_type is "N_A" for STT responses and buildSessionTransferRedirect requires the exact URN.
…oken test, update types - Remove duplicate 'strips protocol' audience test — identical assertion to the preceding 'sets audience' test - Add test for actor_unavailable when session exists but has no id_token - Update index.d.ts: document HTTP 500 invalid_token_response error and make https enforcement explicit (not just advisory) in JSDoc
|
|
||
| > **Prerequisites (configured via Management API / Dashboard):** | ||
| > | ||
| > - Feature flag `cte_session_transfer_token` must be enabled on the tenant. |
There was a problem hiding this comment.
We can avoid calling out the flag name as it's an internal detail
Summary
Adds Custom Token Exchange (CTE) — Impersonation via Session Transfer.
This extends the existing
req.oidc.customTokenExchangesurface with two new methods that implement the initiator role of the session transfer flow: an application requests a short-lived, single-use Session Transfer Token (STT) via CTE, then redirects the user's browser to a target app's login URL carrying that STT, which is redeemed at/authorizeto establish an ephemeral web session as the subject user with the actor recorded in theactclaim.New methods on
req.oidcrequestSessionTransferToken(options)Performs a token exchange against the
urn:{domain}:session_transferaudience and returns aSessionTransferTokenResult. The SDK handles the protocol plumbing automatically:audiencefrom the configuredissuerBaseURLhostnameactor_tokenfrom the current sessionid_token, checking the id_token's ownexpclaim (with a 30s skew). If expired and a refresh token is available, the session is refreshed automatically; if the refresh returns no new id_token, raisesactor_unavailable. If expired with no refresh token, raisesactor_unavailablebefore any network call. An explicitactor_tokenoverride is accepted to bypass session sourcing entirely.actor_unavailable(HTTP 400) client-side before any network call when no actor can be resolved.issued_token_type === "urn:auth0:params:oauth:token-type:session_transfer_token"— an unexpected or missing value raises HTTP 500 (invalid_token_response).buildSessionTransferRedirect(targetLoginUrl, result, opts?)Pure URL builder — appends
session_transfer_token(and optionallyorganization) as query parameters totargetLoginUrl. Makes no network call and writes nothing to the session. The caller passes the returned string tores.redirect().Target side (no SDK changes needed)
The existing login path already forwards
authorizationParamsto/authorize, so redeeming an inbound STT requires no new SDK code:New TypeScript types
SessionTransferTokenResult— result type returned byrequestSessionTransferTokenSessionTransferTokenOptions— input options forrequestSessionTransferTokenSessionTransferRedirectOptions— options forbuildSessionTransferRedirectError codes
actor_unavailableid_token(not logged in, expired with no refresh token, refresh returned no new id_token, or noid_tokenin session)setactor_requiredsetActorsession_transfer_disabledinvalid_token_responseissued_token_typeImplementation notes
customTokenExchangetests continue to pass.validateSubjectToken,validateTokenExchangeExtras,TOKEN_EXCHANGE_GRANT_TYPE, and thegenericGrantRequestCTE machinery.isIdTokenExpired(exp)added tolib/utils/sessionExpiry.js, reusing the existingSESSION_EXPIRY_LEEWAY(30s skew). Guards against non-numericexp(fails closed — treats as expired).SESSION_TRANSFER_TOKEN_IDENTIFIERdefined as an internal constant inlib/context.js— not exported; the URN is documented in EXAMPLES.md and the type declarations for developers who need to branch onissued_token_type.Test plan
test/sessionTransferToken.tests.jscovering: actor resolution (session sourcing, explicit override, blank actor, no session, no id_token in session, expired id_token with and without refresh token, refresh returning no new id_token, non-numeric exp),subject_tokenvalidation, audience construction, optional params (scope,organization,extra), result shape (STT insession_transfer_token, noaccess_token, noact),issued_token_typevalidation at exchange (unexpected and missing both throw 500), no-persistence guarantee, server error code mapping, andbuildSessionTransferRedirectURL construction (including https enforcement and all three loopback variants).