From 8d9da901644bcd7dfb0c52e99a79442481ba0417 Mon Sep 17 00:00:00 2001 From: Snehil Kishore Date: Fri, 26 Jun 2026 18:39:59 +0530 Subject: [PATCH 1/2] feat: add Session Transfer Token (STT) support for impersonation via session transfer (PoC) Adds the initiator surface for CTE Impersonation via Session Transfer (Release 2): - request_session_transfer_token(): mints an STT via custom token exchange against the urn:{domain}:session_transfer audience (domain resolved per-request for MCD) - build_session_transfer_redirect(): builds the redirect URL handing the STT to the target app's login URL (reuses URL.build_url) - SessionTransferTokenResult model and SESSION_TRANSFER_TOKEN_TYPE constant - CustomTokenExchangeErrorCode: ACTOR_UNAVAILABLE, SETACTOR_REQUIRED, SESSION_TRANSFER_DISABLED Additive and non-breaking. Explicit-actor scope; auto-source-from-session and tests deferred. --- .../auth_server/server_client.py | 103 ++++++++++++++++++ .../auth_types/__init__.py | 19 ++++ src/auth0_server_python/error/__init__.py | 3 + 3 files changed, 125 insertions(+) diff --git a/src/auth0_server_python/auth_server/server_client.py b/src/auth0_server_python/auth_server/server_client.py index 83c673d..fada1cd 100644 --- a/src/auth0_server_python/auth_server/server_client.py +++ b/src/auth0_server_python/auth_server/server_client.py @@ -28,6 +28,7 @@ ListConnectedAccountsResponse, LoginWithCustomTokenExchangeOptions, LoginWithCustomTokenExchangeResult, + SessionTransferTokenResult, LogoutOptions, LogoutTokenClaims, MfaRequirements, @@ -73,6 +74,9 @@ INTERNAL_AUTHORIZE_PARAMS = ["client_id", "response_type", "code_challenge", "code_challenge_method", "state", "nonce", "scope"] +# issued_token_type URN for a Session Transfer Token (STT). +SESSION_TRANSFER_TOKEN_TYPE = "urn:auth0:params:oauth:token-type:session_transfer_token" + class ServerClient(Generic[TStoreOptions]): """ @@ -2574,6 +2578,105 @@ async def login_with_custom_token_exchange( e ) + # ============================================================================ + # SESSION TRANSFER TOKEN (STT) + # Impersonation via Session Transfer, built on Custom Token Exchange. + # ============================================================================ + + async def request_session_transfer_token( + self, + subject_token: str, + subject_token_type: str, + actor_token: str, + actor_token_type: str = "urn:ietf:params:oauth:token-type:id_token", + scope: Optional[str] = None, + organization: Optional[str] = None, + store_options: Optional[dict[str, Any]] = None + ) -> SessionTransferTokenResult: + """ + Requests a Session Transfer Token (STT) for impersonation via session transfer. + + Performs a custom token exchange against the session_transfer audience. The returned + STT is opaque and single-use; hand it to build_session_transfer_redirect and do not + decode or store it. The act claim is not on this result. + + Args: + subject_token: Your proof of which customer to impersonate (validated by your Action) + subject_token_type: The subject token type URI routing to your CTE Profile + actor_token: The acting party's token (e.g. the agent's ID token); required + actor_token_type: Type of actor token (defaults to the ID token URN) + scope: Space-delimited list of scopes (optional) + organization: Organization identifier (optional) + store_options: Optional options used to resolve the request domain + + Returns: + SessionTransferTokenResult containing the STT and its metadata + + Raises: + CustomTokenExchangeError: If no actor is provided or the exchange fails + """ + try: + if not actor_token or not actor_token.strip(): + raise CustomTokenExchangeError( + CustomTokenExchangeErrorCode.ACTOR_UNAVAILABLE, + "actor_token is required to request a session transfer token" + ) + + # Build the session_transfer audience from the resolved request domain. + domain = await self._resolve_current_domain(store_options) + audience = f"urn:{domain}:session_transfer" + + options = CustomTokenExchangeOptions( + subject_token=subject_token, + subject_token_type=subject_token_type, + audience=audience, + scope=scope, + actor_token=actor_token, + actor_token_type=actor_token_type, + organization=organization, + ) + + response = await self.custom_token_exchange(options, store_options) + + return SessionTransferTokenResult( + session_transfer_token=response.access_token, + issued_token_type=response.issued_token_type or SESSION_TRANSFER_TOKEN_TYPE, + expires_in=response.expires_in, + token_type=response.token_type, + scope=response.scope, + ) + except (CustomTokenExchangeError, ApiError): + raise + except Exception as e: + raise CustomTokenExchangeError( + CustomTokenExchangeErrorCode.TOKEN_EXCHANGE_FAILED, + f"Session transfer token request failed: {str(e)}", + e + ) + + def build_session_transfer_redirect( + self, + target_login_url: str, + result: SessionTransferTokenResult, + organization: Optional[str] = None + ) -> str: + """ + Builds the redirect URL that hands the STT to the target app's login URL. + + Args: + target_login_url: The target app's login URL + result: The SessionTransferTokenResult from request_session_transfer_token + organization: Organization identifier to forward (optional) + + Returns: + A URL string with session_transfer_token (and organization) as query parameters + """ + params = {"session_transfer_token": result.session_transfer_token} + if organization: + params["organization"] = organization + + return URL.build_url(target_login_url, params) + # ============================================================================ # MFA (Multi-Factor Authentication) # ============================================================================ diff --git a/src/auth0_server_python/auth_types/__init__.py b/src/auth0_server_python/auth_types/__init__.py index fbfe2fe..f57814b 100644 --- a/src/auth0_server_python/auth_types/__init__.py +++ b/src/auth0_server_python/auth_types/__init__.py @@ -321,6 +321,25 @@ class LoginWithCustomTokenExchangeResult(BaseModel): state_data: dict[str, Any] authorization_details: Optional[list[AuthorizationDetails]] = None + +class SessionTransferTokenResult(BaseModel): + """ + Response from a session transfer token (STT) request. + + Attributes: + session_transfer_token: The opaque, single-use session transfer token + issued_token_type: Format of issued token (the session-transfer URN) + expires_in: Token lifetime in seconds + token_type: Token type as returned by the server (typically "N_A") + scope: Granted scopes (if returned) + """ + session_transfer_token: str + issued_token_type: str + expires_in: int + token_type: Optional[str] = None + scope: Optional[str] = None + + # ============================================================================= # Connected Accounts Types # ============================================================================= diff --git a/src/auth0_server_python/error/__init__.py b/src/auth0_server_python/error/__init__.py index 407435f..7c5e576 100644 --- a/src/auth0_server_python/error/__init__.py +++ b/src/auth0_server_python/error/__init__.py @@ -240,6 +240,9 @@ class CustomTokenExchangeErrorCode: MISSING_ACTOR_TOKEN = "missing_actor_token" TOKEN_EXCHANGE_FAILED = "token_exchange_failed" INVALID_RESPONSE = "invalid_response" + ACTOR_UNAVAILABLE = "actor_unavailable" + SETACTOR_REQUIRED = "setactor_required" + SESSION_TRANSFER_DISABLED = "session_transfer_disabled" # ============================================================================= From ffa918f1fb546f8dfa570895c53b13e497efeb42 Mon Sep 17 00:00:00 2001 From: Snehil Kishore Date: Fri, 26 Jun 2026 18:59:50 +0530 Subject: [PATCH 2/2] refactor: make actor_token_type required and fix import ordering actor_token_type is caller-supplied, so it no longer defaults to the id_token URN - the caller states the type matching their actor token (consistent with custom_token_exchange). Also sorts the auth_types import block (ruff I001). Lint clean. --- src/auth0_server_python/auth_server/server_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auth0_server_python/auth_server/server_client.py b/src/auth0_server_python/auth_server/server_client.py index fada1cd..50d860f 100644 --- a/src/auth0_server_python/auth_server/server_client.py +++ b/src/auth0_server_python/auth_server/server_client.py @@ -28,10 +28,10 @@ ListConnectedAccountsResponse, LoginWithCustomTokenExchangeOptions, LoginWithCustomTokenExchangeResult, - SessionTransferTokenResult, LogoutOptions, LogoutTokenClaims, MfaRequirements, + SessionTransferTokenResult, StartInteractiveLoginOptions, StateData, TokenExchangeResponse, @@ -2588,7 +2588,7 @@ async def request_session_transfer_token( subject_token: str, subject_token_type: str, actor_token: str, - actor_token_type: str = "urn:ietf:params:oauth:token-type:id_token", + actor_token_type: str, scope: Optional[str] = None, organization: Optional[str] = None, store_options: Optional[dict[str, Any]] = None @@ -2604,7 +2604,7 @@ async def request_session_transfer_token( subject_token: Your proof of which customer to impersonate (validated by your Action) subject_token_type: The subject token type URI routing to your CTE Profile actor_token: The acting party's token (e.g. the agent's ID token); required - actor_token_type: Type of actor token (defaults to the ID token URN) + actor_token_type: Type URI of the actor token (e.g. the ID token URN); required scope: Space-delimited list of scopes (optional) organization: Organization identifier (optional) store_options: Optional options used to resolve the request domain