Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions src/auth0_server_python/auth_server/server_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
LogoutOptions,
LogoutTokenClaims,
MfaRequirements,
SessionTransferTokenResult,
StartInteractiveLoginOptions,
StateData,
TokenExchangeResponse,
Expand Down Expand Up @@ -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]):
"""
Expand Down Expand Up @@ -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,
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 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

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)
# ============================================================================
Expand Down
19 changes: 19 additions & 0 deletions src/auth0_server_python/auth_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =============================================================================
Expand Down
3 changes: 3 additions & 0 deletions src/auth0_server_python/error/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


# =============================================================================
Expand Down
Loading