Skip to content

Commit 303f408

Browse files
feat: Add Custom Token Exchange support (#70)
* feat(auth): Add custom token exchange types and error classes * feat(auth): Implement custom token exchange methods * test(auth): Add comprehensive tests for custom token exchange * docs(auth): Add examples for custom token exchange in README and new documentation file * docs(auth): Update Custom Token Exchange documentation for clarity and structure * feat(auth): Add organization support to custom token exchange options and update related documentation * test(auth): Update custom token exchange tests to raise ValidationError for invalid tokens * feat(auth): Add organization parameter to ServerClient for custom token exchange * fix: import order issue * fix: reorder import statements for consistency --------- Co-authored-by: Snehil Kishore <snehil.kishore@okta.com>
1 parent 0a65953 commit 303f408

7 files changed

Lines changed: 1253 additions & 1 deletion

File tree

.snyk

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ ignore:
2121
- '*':
2222
reason: "Accepting the Unknown license for now"
2323
expires: "2030-12-31T23:59:59Z"
24+
"snyk:lic:pip:cryptography:Unknown":
25+
- '*':
26+
reason: "Accepting the Unknown license for now"
27+
expires: "2030-12-31T23:59:59Z"
2428
patch: {}

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,47 @@ async def callback(request: Request):
104104
return RedirectResponse(url="/")
105105
```
106106

107+
### 4. Login with Custom Token Exchange
108+
109+
If you're migrating from a legacy authentication system or integrating with a custom identity provider, you can exchange external tokens for Auth0 tokens using the OAuth 2.0 Token Exchange specification (RFC 8693):
110+
111+
```python
112+
from auth0_server_python.auth_types import LoginWithCustomTokenExchangeOptions
113+
114+
# Exchange a custom token and establish a session
115+
result = await auth0.login_with_custom_token_exchange(
116+
LoginWithCustomTokenExchangeOptions(
117+
subject_token="your-custom-token",
118+
subject_token_type="urn:acme:mcp-token",
119+
audience="https://api.example.com"
120+
),
121+
store_options={"request": request, "response": response}
122+
)
123+
124+
# Access the user session
125+
user = result.state_data["user"]
126+
```
127+
128+
For advanced token exchange scenarios (without creating a session), use `custom_token_exchange()` directly:
129+
130+
```python
131+
from auth0_server_python.auth_types import CustomTokenExchangeOptions
132+
133+
# Exchange a custom token for Auth0 tokens
134+
response = await auth0.custom_token_exchange(
135+
CustomTokenExchangeOptions(
136+
subject_token="your-custom-token",
137+
subject_token_type="urn:acme:mcp-token",
138+
audience="https://api.example.com",
139+
scope="read:data write:data"
140+
)
141+
)
142+
143+
print(response.access_token)
144+
```
145+
146+
For more details and examples, see [examples/CustomTokenExchange.md](examples/CustomTokenExchange.md).
147+
107148
## Feedback
108149

109150
### Contributing

examples/CustomTokenExchange.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Custom Token Exchange
2+
3+
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](https://datatracker.ietf.org/doc/html/rfc8693)).
4+
5+
> **NOTE**: For complete documentation on Custom Token Exchange, configuration requirements, and detailed use cases, see the [official Auth0 documentation](https://auth0.com/docs/authenticate/custom-token-exchange).
6+
7+
## 1. Basic Token Exchange
8+
9+
Exchange a custom token for Auth0 tokens without creating a user session.
10+
11+
```python
12+
from auth0_server_python.auth_server.server_client import ServerClient
13+
from auth0_server_python.auth_types import CustomTokenExchangeOptions
14+
15+
# Initialize the client
16+
auth0 = ServerClient(
17+
domain="<AUTH0_DOMAIN>",
18+
client_id="<AUTH0_CLIENT_ID>",
19+
client_secret="<AUTH0_CLIENT_SECRET>",
20+
secret="<AUTH0_SECRET>"
21+
)
22+
23+
# Exchange a custom token
24+
response = await auth0.custom_token_exchange(
25+
CustomTokenExchangeOptions(
26+
subject_token="custom-token-from-external-system",
27+
subject_token_type="urn:acme:mcp-token",
28+
audience="https://api.example.com",
29+
scope="read:data write:data"
30+
)
31+
)
32+
33+
# Access the exchanged tokens
34+
print(f"Access Token: {response.access_token}")
35+
print(f"Expires In: {response.expires_in} seconds")
36+
if response.id_token:
37+
print(f"ID Token: {response.id_token}")
38+
```
39+
40+
## 2. Login with Token Exchange
41+
42+
Exchange a custom token AND establish a user session.
43+
44+
```python
45+
from auth0_server_python.auth_types import LoginWithCustomTokenExchangeOptions
46+
from fastapi import Request, Response
47+
48+
# Exchange token and create session
49+
result = await auth0.login_with_custom_token_exchange(
50+
LoginWithCustomTokenExchangeOptions(
51+
subject_token="custom-token-from-external-system",
52+
subject_token_type="urn:acme:mcp-token",
53+
audience="https://api.example.com"
54+
),
55+
store_options={"request": request, "response": response}
56+
)
57+
58+
# User is now logged in
59+
user = result.state_data["user"]
60+
print(f"User logged in: {user['sub']}")
61+
```
62+
63+
> **TIP**: Use `login_with_custom_token_exchange()` when you need both token exchange and session management (e.g., user migration flows). Use `custom_token_exchange()` for pure token exchange scenarios (e.g., service-to-service authentication).
64+
65+
## 3. Actor Tokens (Delegation)
66+
67+
Enable delegation scenarios where one service acts on behalf of a user.
68+
69+
```python
70+
# Service acting on behalf of a user
71+
response = await auth0.custom_token_exchange(
72+
CustomTokenExchangeOptions(
73+
subject_token="user-access-token",
74+
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
75+
actor_token="service-access-token",
76+
actor_token_type="urn:ietf:params:oauth:token-type:access_token",
77+
audience="https://api.example.com"
78+
)
79+
)
80+
```
81+
82+
## 4. Custom Authorization Parameters
83+
84+
Pass additional parameters to the token endpoint.
85+
86+
```python
87+
response = await auth0.custom_token_exchange(
88+
CustomTokenExchangeOptions(
89+
subject_token="custom-token",
90+
subject_token_type="urn:acme:mcp-token",
91+
audience="https://api.example.com",
92+
authorization_params={
93+
"custom_field": "custom_value"
94+
}
95+
)
96+
)
97+
```
98+
99+
> **NOTE**: Critical parameters (`grant_type`, `client_id`, `subject_token`, `subject_token_type`) cannot be overridden via `authorization_params` for security reasons.
100+
101+
## 5. Organization Support
102+
103+
Specify an organization when exchanging tokens.
104+
105+
```python
106+
response = await auth0.custom_token_exchange(
107+
CustomTokenExchangeOptions(
108+
subject_token="custom-token",
109+
subject_token_type="urn:acme:mcp-token",
110+
audience="https://api.example.com",
111+
organization="org_abc1234"
112+
)
113+
)
114+
```
115+
116+
## 6. Error Handling
117+
118+
```python
119+
from auth0_server_python.error import CustomTokenExchangeError
120+
121+
try:
122+
response = await auth0.custom_token_exchange(
123+
CustomTokenExchangeOptions(
124+
subject_token="token",
125+
subject_token_type="urn:acme:mcp-token"
126+
)
127+
)
128+
except CustomTokenExchangeError as e:
129+
print(f"Exchange failed: {e.code} - {e.message}")
130+
```
131+
132+
### Common Error Codes
133+
134+
- `INVALID_TOKEN_FORMAT`: Token is empty, whitespace-only, or has "Bearer " prefix
135+
- `MISSING_ACTOR_TOKEN_TYPE`: `actor_token` provided without `actor_token_type`
136+
- `TOKEN_EXCHANGE_FAILED`: General token exchange failure
137+
- `INVALID_RESPONSE`: Auth0 returned a non-JSON response
138+
139+
## 7. Token Type URIs
140+
141+
Use standard URNs when possible:
142+
143+
```python
144+
# Standard token types
145+
"urn:ietf:params:oauth:token-type:jwt" # JWT tokens
146+
"urn:ietf:params:oauth:token-type:access_token" # OAuth access tokens
147+
"urn:ietf:params:oauth:token-type:id_token" # OpenID Connect ID tokens
148+
"urn:ietf:params:oauth:token-type:refresh_token" # OAuth refresh tokens
149+
150+
# Custom token types (use your own namespace)
151+
"urn:acme:mcp-token"
152+
"urn:company:legacy-token"
153+
```
154+
155+
## Additional Resources
156+
157+
- [Auth0 Custom Token Exchange Documentation](https://auth0.com/docs/authenticate/custom-token-exchange)
158+
- [RFC 8693 - OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)

0 commit comments

Comments
 (0)