feat: add Session Transfer Token support for CTE impersonation via session transfer#139
feat: add Session Transfer Token support for CTE impersonation via session transfer#139kishore7snehil wants to merge 2 commits into
Conversation
…via session transfer
Adds the initiator surface for Impersonation via Session Transfer on top of
Custom Token Exchange:
- request_session_transfer_token: mints an STT against the urn:{domain}:session_transfer
audience. The actor is auto-sourced from the agent session's ID token (verified via
JWKS, refreshed when expired) and overridable via an explicit actor_token; a blank
actor_token is rejected, and no usable actor fails client-side with ACTOR_UNAVAILABLE
before any network call.
- build_session_transfer_redirect: builds the URL-encoded redirect that hands the STT to
the target app's login URL (organization forwarded when present).
- SessionTransferTokenResult model and the ACTOR_UNAVAILABLE / SETACTOR_REQUIRED /
SESSION_TRANSFER_DISABLED error codes.
- Developer guide (examples/CustomTokenExchange.md) + README pointer, and a Session
Transfer Token test section covering actor resolution, the refresh path, stateless
minting, error cases, and the redirect builder.
Target-side redemption needs no new SDK code: start_interactive_login already forwards
session_transfer_token to /authorize.
| if organization: | ||
| params["organization"] = organization | ||
|
|
||
| return URL.build_url(target_login_url, params) |
There was a problem hiding this comment.
build_session_transfer_redirect builds the URL without validating target_login_url. The STT is a single use credential, so if this value ever comes from untrusted input it can leak the token to another host. Can we check that the URL is absolute and uses https here (allowing http only for localhost/127.0.0.1/[::1] for local dev)? The docstring says nothing about this, and I think we should enforce it in code rather than trust the caller. Right now anything that URL.build_url accepts will go through.
|
|
||
| return SessionTransferTokenResult( | ||
| session_transfer_token=response.access_token, | ||
| issued_token_type=response.issued_token_type or SESSION_TRANSFER_TOKEN_TYPE, |
There was a problem hiding this comment.
We fall back to the hardcoded URN with response.issued_token_type or SESSION_TRANSFER_TOKEN_TYPE. I would avoid fabricating this value, because if the server ever returns a response without issued_token_type (or a non STT one), we would still label it as an STT. Since the whole point is that callers branch on issued_token_type, can we surface exactly what the server returned instead of defaulting it?
| TOKEN_EXCHANGE_FAILED = "token_exchange_failed" | ||
| INVALID_RESPONSE = "invalid_response" | ||
| ACTOR_UNAVAILABLE = "actor_unavailable" | ||
| SETACTOR_REQUIRED = "setactor_required" |
There was a problem hiding this comment.
SETACTOR_REQUIRED and SESSION_TRANSFER_DISABLED are defined here and mentioned in the docs, but I could not find anywhere they are actually raised. So today a real server 400 for these cases would surface as TOKEN_EXCHANGE_FAILED, not the typed code, and a developer catching on these constants would never hit them. Can we map the server error to these codes where the exchange fails (or if that is intentionally deferred, add a short comment saying they are placeholders for now)?
There was a problem hiding this comment.
These aren't raised by any SDK path on purpose. I checked the server (CustomTokenExchangeProfile.js and its custom_token_exchange.n2w.tests.js): all three STT rejections come back as error: "invalid_request" with the reason only in error_description ("setActor is required when requesting a session transfer token via token exchange.", "Feature is disabled for this client", etc.). The strings setactor_required/session_transfer_disabled don't exist anywhere in the server — it never emits them as codes.
The only way to set our typed code would be to string-match that error_description, which is brittle (a server reword silently breaks it), so we surface the raw server error/error_description as-is — the same as our existing CTE failures and the sibling auth0-auth-js STT PR. Keeping the two constants as named references for the conditions; happy to drop them if you'd rather not have unused constants.
| domain = await self._resolve_current_domain(store_options) | ||
| metadata = await self._get_oidc_metadata_cached(domain) | ||
| jwks = await self._get_jwks_cached(domain, metadata) | ||
| await self._verify_and_decode_jwt(token, jwks, audience=self._client_id) |
There was a problem hiding this comment.
_is_id_token_usable verifies the ID token with audience=self._client_id. That is correct for a standard Auth0 ID token, just flagging it as an assumption: if a tenant ever issues the session ID token with a different aud, this verification would fail and the token would be treated as unusable (leading to a refresh or ACTOR_UNAVAILABLE). Probably fine, but worth a comment so it is a conscious choice.
| self.DEFAULT_AUDIENCE_STATE_KEY, state_data, refreshed) | ||
| await self._state_store.set(self._state_identifier, updated_state_data, options=store_options) | ||
| session_id_token = refreshed.get("id_token") or session_id_token | ||
| except Exception: |
There was a problem hiding this comment.
Small one: the refresh here swallows every exception with a bare except Exception and sets session_id_token = None, which then falls through to ACTOR_UNAVAILABLE. That is a reasonable outcome, but silently eating all errors can hide real problems (network, config, a bug in the refresh path). Could we at least log the caught error before falling back, so failures are debuggable?
- Validate the redirect target URL in build_session_transfer_redirect: require an absolute https URL (http allowed only for localhost/loopback), via a reusable URL.validate_https_redirect_target helper. Prevents leaking the single-use STT to an untrusted host. - Surface issued_token_type exactly as the server returned it instead of defaulting to the STT URN, so a non-STT response is not mislabelled. - Validate subject_token/subject_token_type up front, before any session read or network. - Narrow the swallowed exceptions in actor resolution: the refresh catches only ApiError/AccessTokenError and _is_id_token_usable only token-verification errors, so infrastructure failures propagate instead of being masked as ACTOR_UNAVAILABLE. - Source the refresh domain from the session (resolver-safe) and reject an actor sourced from a session on a different domain in resolver mode. - Note the aud==client_id assumption in _is_id_token_usable. - Add tests for redirect validation, issued_token_type passthrough, up-front subject validation, and the actor-resolution paths.
📋 Changes
This PR adds the initiator surface for Custom Token Exchange (CTE) Impersonation via Session Transfer (RFC 8693) to auth0-server-python. An application can exchange a subject token for a short-lived, single-use Session Transfer Token (STT) that redeems into an impersonated web session on a target app - logging an agent in as a customer with the agent recorded in the
actclaim. The SDK mints the STT and builds the redirect; it never decodes or stores the token.✨ Features
request_session_transfer_tokenperforms the CTE exchange against theurn:{domain}:session_transferaudience, built from the request-resolved domain (MCD-compatible), and returns aSessionTransferTokenResultactor_tokenoverrides itactor_tokenis rejected withINVALID_TOKEN_FORMAT, and when no usable actor can be resolved the call fails client-side withACTOR_UNAVAILABLEbefore any network requestbuild_session_transfer_redirectreturns the URL-encoded redirect that hands the STT to the target app's login URL, forwardingorganizationwhen present🔧 API Changes
request_session_transfer_token(subject_token, subject_token_type, actor_token=None, actor_token_type=None, scope=None, organization=None, store_options=None) -> SessionTransferTokenResultbuild_session_transfer_redirect(target_login_url, result, organization=None) -> strSessionTransferTokenResultmodel (session_transfer_token,issued_token_type,expires_in,token_type,scope)CustomTokenExchangeErrorCode.ACTOR_UNAVAILABLE,SETACTOR_REQUIRED,SESSION_TRANSFER_DISABLED📖 Documentation
examples/CustomTokenExchange.mdcovering the initiator and target roles, actor sourcing and override, the redirect-URL security note, and the STT error codesREADME.md🧪 Testing
Contributor Checklist