This document provides examples for using the auth0-api-python package to validate Auth0 tokens in your API.
Use get_token_on_behalf_of() when your API receives an Auth0 access token for itself and needs
to exchange it for another Auth0 access token targeting a downstream API while preserving the same
user identity. This is especially useful for MCP servers and other intermediary APIs that need to
call downstream APIs on behalf of the user.
The following example verifies the incoming access token for your API, exchanges it for a token for the downstream API, and then calls the downstream API with the exchanged token.
import asyncio
import httpx
from auth0_api_python import ApiClient, ApiClientOptions
async def exchange_on_behalf_of():
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://mcp-server.example.com",
client_id="<AUTH0_CLIENT_ID>",
client_secret="<AUTH0_CLIENT_SECRET>"
))
incoming_access_token = "incoming-auth0-access-token"
claims = await api_client.verify_access_token(access_token=incoming_access_token)
result = await api_client.get_token_on_behalf_of(
access_token=incoming_access_token,
audience="https://calendar-api.example.com",
scope="calendar:read calendar:write"
)
async with httpx.AsyncClient() as client:
downstream_response = await client.get(
"https://calendar-api.example.com/events",
headers={"Authorization": f"Bearer {result['access_token']}"}
)
downstream_response.raise_for_status()
return {
"user": claims["sub"],
"data": downstream_response.json(),
}
asyncio.run(exchange_on_behalf_of())[!TIP] Production notes:
- Pass the raw access token to
get_token_on_behalf_of(). Do not pass the fullAuthorizationheader or include theBearerprefix.- Verify the incoming token for your API before exchanging it so your application rejects invalid or mis-targeted tokens early.
- The downstream
audiencemust match an API identifier configured in your Auth0 tenant.get_token_on_behalf_of()only returns access-token-oriented fields. It does not exposeid_tokenorrefresh_token.
In the current implementation, get_token_on_behalf_of() forwards the incoming access token as
the RFC 8693 subject_token and relies on Auth0 to handle any DPoP-specific behavior for that token.
When a downstream API or MCP server receives an access token that may have been issued through
delegation, it can verify the token first and then inspect the act claim to identify the current
actor for authorization and the full delegation chain for audit or attribution.
import asyncio
import logging
from auth0_api_python import (
ApiClient,
ApiClientOptions,
get_current_actor,
get_delegation_chain,
)
logger = logging.getLogger(__name__)
async def inspect_delegated_token():
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://calendar-api.example.com"
))
access_token = "delegated-auth0-access-token"
claims = await api_client.verify_access_token(access_token=access_token)
current_actor = get_current_actor(claims)
delegation_chain = get_delegation_chain(claims)
if current_actor != "mcp_server_client_id":
raise PermissionError("unexpected actor")
logger.info(
"delegated request",
extra={
"user_sub": claims["sub"],
"current_actor": current_actor,
"delegation_chain": delegation_chain,
},
)
return {
"user_sub": claims["sub"],
"current_actor": current_actor,
"delegation_chain": delegation_chain,
}
asyncio.run(inspect_delegated_token())Only the outermost act.sub represents the current actor and should be used for authorization
decisions. Nested act values represent prior actors and are better suited for logging, audit, or
attribution.
Bearer authentication is the standard OAuth 2.0 token authentication method.
import asyncio
from auth0_api_python import ApiClient, ApiClientOptions
async def validate_bearer_token(headers):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))
try:
# Extract the token from the Authorization header
auth_header = headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
return {"error": "Missing or invalid authorization header"}, 401
token = auth_header.split(" ")[1]
# Verify the access token
claims = await api_client.verify_access_token(token)
return {"success": True, "user": claims["sub"]}
except Exception as e:
return {"error": str(e)}, getattr(e, "get_status_code", lambda: 401)()
# Example usage
headers = {"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."}
result = asyncio.run(validate_bearer_token(headers))import asyncio
from auth0_api_python import ApiClient, ApiClientOptions
from auth0_api_python.errors import BaseAuthError
async def validate_request(headers):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))
try:
# Verify the request with Bearer token
claims = await api_client.verify_request(
headers=headers
)
return {"success": True, "user": claims["sub"]}
except BaseAuthError as e:
return {"error": str(e)}, e.get_status_code(), e.get_headers()
# Example usage
headers = {"authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."}
result = asyncio.run(validate_request(headers))DPoP (Demonstrating Proof of Posession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the client application is in possession of a certain private key.
This guide covers the DPoP implementation in auth0-api-python with complete examples for both operational modes.
For more information about DPoP specification, see RFC 9449.
from auth0_api_python import ApiClient, ApiClientOptions
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com",
dpop_enabled=True, # Default: enables DPoP support
dpop_required=False # Default: allows both Bearer and DPoP
))api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com",
dpop_required=True # Enforces DPoP-only authentication
))The verify_request() method automatically detects the authentication scheme:
import asyncio
from auth0_api_python import ApiClient, ApiClientOptions
async def handle_api_request(headers, http_method, http_url):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))
try:
# Automatically handles both Bearer and DPoP schemes
claims = await api_client.verify_request(
headers=headers,
http_method=http_method,
http_url=http_url
)
return {"success": True, "user": claims["sub"]}
except Exception as e:
return {"error": str(e)}, e.get_status_code()
# Example usage
headers = {
"authorization": "DPoP eyJ0eXAiOiJKV1Q...",
"dpop": "eyJ0eXAiOiJkcG9wK2p3dC..."
}
result = asyncio.run(handle_api_request(headers, "GET", "https://api.example.com/data"))For more control, use verify_dpop_proof() directly:
async def verify_dpop_token(access_token, dpop_proof, http_method, http_url):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))
# First verify the access token
token_claims = await api_client.verify_access_token(access_token)
# Then verify the DPoP proof
proof_claims = await api_client.verify_dpop_proof(
access_token=access_token,
proof=dpop_proof,
http_method=http_method,
http_url=http_url
)
return {
"token_claims": token_claims,
"proof_claims": proof_claims
}Anonymous Sessions give a visitor an Auth0 identity before they log in. The access token issued for an anonymous session is a standard Auth0 Bearer JWT, so this SDK validates it like any other token. The one difference is the sub claim, which starts with anon@.
An anonymous token is verified like any other. It must be issued for this API's audience. To treat anonymous callers differently, or block them, check the sub claim after verifying the token. The SDK does not make that authorization decision for you.
from auth0_api_python import ApiClient, ApiClientOptions
async def handle_cart(headers):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))
claims = await api_client.verify_request(headers=headers)
is_anonymous = claims.get("sub", "").startswith("anon@")
if is_anonymous:
return {"cart": load_guest_cart(claims["sub"])}
return {"cart": load_user_cart(claims["sub"])}Note
These snippets construct ApiClient inside the handler for clarity. In production, build it once at startup and reuse it, or pass a shared cache_adapter, so JWKS and discovery caches persist across requests.
from auth0_api_python import ApiClient, ApiClientOptions
async def handle_checkout(headers):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))
claims = await api_client.verify_request(headers=headers)
if claims.get("sub", "").startswith("anon@"):
raise PermissionError("Anonymous callers are not allowed on this route")
return {"order": create_order(claims["sub"])}Note
Blocking an anonymous caller should return an HTTP 403 Forbidden. Replace PermissionError with your framework's error type.
The SDK has no global "reject anonymous" switch. Centralize the check in whatever shared layer your framework uses for auth (middleware, a FastAPI dependency, a decorator).
from auth0_api_python import ApiClient, ApiClientOptions
async def require_logged_in_user(headers):
api_client = ApiClient(ApiClientOptions(
domain="your-tenant.auth0.com",
audience="https://api.example.com"
))
claims = await api_client.verify_request(headers=headers)
if claims.get("sub", "").startswith("anon@"):
raise PermissionError("Anonymous callers are not allowed")
return claimsNote
The anon@ prefix on sub is the only signal that distinguishes an anonymous caller from a logged-in user.