Custom Token Exchange allows you to exchange tokens from external identity providers or legacy authentication systems for Auth0 tokens without browser redirects. This implements OAuth 2.0 Token Exchange (RFC 8693).
NOTE: For complete documentation on Custom Token Exchange, configuration requirements, and detailed use cases, see the official Auth0 documentation.
Exchange a custom token for Auth0 tokens without creating a user session.
from auth0_server_python.auth_server.server_client import ServerClient
from auth0_server_python.auth_types import CustomTokenExchangeOptions
# Initialize the client
auth0 = ServerClient(
domain="<AUTH0_DOMAIN>",
client_id="<AUTH0_CLIENT_ID>",
client_secret="<AUTH0_CLIENT_SECRET>",
secret="<AUTH0_SECRET>"
)
# Exchange a custom token
response = await auth0.custom_token_exchange(
CustomTokenExchangeOptions(
subject_token="custom-token-from-external-system",
subject_token_type="urn:acme:mcp-token",
audience="https://api.example.com",
scope="read:data write:data"
)
)
# Access the exchanged tokens
print(f"Access Token: {response.access_token}")
print(f"Expires In: {response.expires_in} seconds")
if response.id_token:
print(f"ID Token: {response.id_token}")Exchange a custom token AND establish a user session.
from auth0_server_python.auth_types import LoginWithCustomTokenExchangeOptions
from fastapi import Request, Response
# Exchange token and create session
result = await auth0.login_with_custom_token_exchange(
LoginWithCustomTokenExchangeOptions(
subject_token="custom-token-from-external-system",
subject_token_type="urn:acme:mcp-token",
audience="https://api.example.com"
),
store_options={"request": request, "response": response}
)
# User is now logged in
user = result.state_data["user"]
print(f"User logged in: {user['sub']}")TIP: Use
login_with_custom_token_exchange()when you need both token exchange and session management (e.g., user migration flows). Usecustom_token_exchange()for pure token exchange scenarios (e.g., service-to-service authentication).
Enable delegation scenarios where one party acts on behalf of a user. The acting party is supplied via actor_token, and Auth0 records it in the act claim on the issued tokens.
# Service acting on behalf of a user
response = await auth0.custom_token_exchange(
CustomTokenExchangeOptions(
subject_token="user-access-token",
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
actor_token="service-access-token",
actor_token_type="urn:ietf:params:oauth:token-type:access_token",
audience="https://api.example.com"
)
)
# The actor claim is surfaced on the response. It may nest for delegation chains.
if response.act:
print(f"Acting party: {response.act['sub']}")NOTE:
response.actis read from the ID token. Auth0 writes the sameactclaim onto the issued access token as well, so they reflect the same acting party. The access token may be opaque, in which caseactcannot be read off it directly - the ID token is where you read it.
When you establish a session with login_with_custom_token_exchange(), the act claim is persisted on the session user and can be read back later via get_user():
result = await auth0.login_with_custom_token_exchange(
LoginWithCustomTokenExchangeOptions(
subject_token="user-access-token",
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
actor_token="service-access-token",
actor_token_type="urn:ietf:params:oauth:token-type:access_token",
),
store_options={"request": request, "response": response}
)
user = result.state_data["user"]
if user.get("act"):
print(f"Acting party: {user['act']['sub']}")NOTE: When an
actor_tokenis present, Auth0 does not issue a refresh token (theoffline_accessscope is dropped). A subsequent refresh-token grant therefore cannot re-emit theactclaim, so the acting party is fixed at exchange time.
Pass additional parameters to the token endpoint.
response = await auth0.custom_token_exchange(
CustomTokenExchangeOptions(
subject_token="custom-token",
subject_token_type="urn:acme:mcp-token",
audience="https://api.example.com",
authorization_params={
"custom_field": "custom_value"
}
)
)NOTE: Critical parameters (
grant_type,client_id,subject_token,subject_token_type) cannot be overridden viaauthorization_paramsfor security reasons.
Specify an organization when exchanging tokens.
response = await auth0.custom_token_exchange(
CustomTokenExchangeOptions(
subject_token="custom-token",
subject_token_type="urn:acme:mcp-token",
audience="https://api.example.com",
organization="org_abc1234"
)
)from auth0_server_python.error import CustomTokenExchangeError
try:
response = await auth0.custom_token_exchange(
CustomTokenExchangeOptions(
subject_token="token",
subject_token_type="urn:acme:mcp-token"
)
)
except CustomTokenExchangeError as e:
print(f"Exchange failed: {e.code} - {e.message}")INVALID_TOKEN_FORMAT: Token is empty, whitespace-only, or has "Bearer " prefixMISSING_ACTOR_TOKEN_TYPE:actor_tokenprovided withoutactor_token_typeMISSING_ACTOR_TOKEN:actor_token_typeprovided withoutactor_tokenTOKEN_EXCHANGE_FAILED: General token exchange failureINVALID_RESPONSE: Auth0 returned a non-JSON response
Use standard URNs when possible:
# Standard token types
"urn:ietf:params:oauth:token-type:jwt" # JWT tokens
"urn:ietf:params:oauth:token-type:access_token" # OAuth access tokens
"urn:ietf:params:oauth:token-type:id_token" # OpenID Connect ID tokens
"urn:ietf:params:oauth:token-type:refresh_token" # OAuth refresh tokens
# Custom token types (use your own namespace)
"urn:acme:mcp-token"
"urn:company:legacy-token"