Skip to content

feat: add Session Transfer Token support for CTE impersonation via session transfer#139

Open
kishore7snehil wants to merge 2 commits into
mainfrom
feat/cte-stt-support
Open

feat: add Session Transfer Token support for CTE impersonation via session transfer#139
kishore7snehil wants to merge 2 commits into
mainfrom
feat/cte-stt-support

Conversation

@kishore7snehil

Copy link
Copy Markdown
Contributor

📋 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 act claim. The SDK mints the STT and builds the redirect; it never decodes or stores the token.

✨ Features

  • request_session_transfer_token performs the CTE exchange against the urn:{domain}:session_transfer audience, built from the request-resolved domain (MCD-compatible), and returns a SessionTransferTokenResult
  • The STT is opaque and single-use; the exchange is stateless and never persists it to the session
  • The actor defaults to the agent's session ID token, verified against JWKS and refreshed when expired, and an explicit actor_token overrides it
  • A passed-but-blank actor_token is rejected with INVALID_TOKEN_FORMAT, and when no usable actor can be resolved the call fails client-side with ACTOR_UNAVAILABLE before any network request
  • build_session_transfer_redirect returns the URL-encoded redirect that hands the STT to the target app's login URL, forwarding organization when present

🔧 API Changes

  • New method request_session_transfer_token(subject_token, subject_token_type, actor_token=None, actor_token_type=None, scope=None, organization=None, store_options=None) -> SessionTransferTokenResult
  • New method build_session_transfer_redirect(target_login_url, result, organization=None) -> str
  • New SessionTransferTokenResult model (session_transfer_token, issued_token_type, expires_in, token_type, scope)
  • New error codes CustomTokenExchangeErrorCode.ACTOR_UNAVAILABLE, SETACTOR_REQUIRED, SESSION_TRANSFER_DISABLED

📖 Documentation

  • Added an "Impersonation via Session Transfer (STT)" section to examples/CustomTokenExchange.md covering the initiator and target roles, actor sourcing and override, the redirect-URL security note, and the STT error codes
  • Linked the new guide from the Custom Token Exchange section of README.md

🧪 Testing

  • This change adds test coverage
  • This change has been tested on the latest version of the platform/language

Contributor Checklist

…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.
@kishore7snehil
kishore7snehil requested a review from a team as a code owner July 21, 2026 09:31
if organization:
params["organization"] = organization

return URL.build_url(target_login_url, params)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a070d41


return SessionTransferTokenResult(
session_transfer_token=response.access_token,
issued_token_type=response.issued_token_type or SESSION_TRANSFER_TOKEN_TYPE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a070d41

TOKEN_EXCHANGE_FAILED = "token_exchange_failed"
INVALID_RESPONSE = "invalid_response"
ACTOR_UNAVAILABLE = "actor_unavailable"
SETACTOR_REQUIRED = "setactor_required"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a070d41

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a070d41

- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants