Skip to content

Commit 5fb9754

Browse files
btiernayclaude
andcommitted
feat: add Custom Token Exchange support (RFC 8693)
Add get_token_by_exchange_profile() method for exchanging subject tokens for Auth0 tokens using RFC 8693 OAuth 2.0 Token Exchange. This enables migration workflows, external IdP integration, and custom token exchange scenarios via Token Exchange Profiles. ## Core Features - RFC 8693 Compliant: Standard OAuth 2.0 token exchange flow - Token Exchange Profiles: subject_token_type determines which profile processes the exchange - HTTP Basic Authentication: client_secret_basic for confidential clients - Configurable Timeout: HTTP timeout support (default: 10.0s) - Extra Parameters: Pass custom form fields with comprehensive validation - Lenient expires_in: Coerces numeric strings like "3600" to int ## Security & Validation - Reserved Parameter Protection: Case-insensitive denylist prevents override of OAuth parameters - DoS Protection: Array size limit (MAX_ARRAY_VALUES_PER_KEY = 20) for extra parameters - Strict Token Validation: Fail-fast checks for whitespace, Bearer prefix, blank tokens - Type Safety: Accepts str or sequences (list, tuple); rejects dict/set with clear errors - Network Efficiency: Early validation prevents unnecessary token endpoint requests ## Error Handling - GetTokenByExchangeProfileError: Validation failures (invalid format, missing credentials, reserved params, unsupported types) - ApiError: Token endpoint failures (invalid_grant, network issues, timeouts, malformed responses) - Both exported at package level: from auth0_api_python import ApiError, GetTokenByExchangeProfileError ## Response Handling - Always returned: access_token, expires_in, expires_at (deterministic calculation) - Optional fields: id_token, refresh_token, scope, token_type, issued_token_type - Preserves falsy values: Empty scope strings from server responses ## API Signature async def get_token_by_exchange_profile( subject_token: str, subject_token_type: str, audience: Optional[str] = None, scope: Optional[str] = None, requested_token_type: Optional[str] = None, extra: Optional[Mapping[str, Union[str, Sequence[str]]]] = None ) -> dict[str, Any] ## Testing - 118 tests with 86% coverage - Comprehensive test suite: - HTTP Basic auth verification - Discovery edge cases (missing endpoint, non-JSON, HTTP errors) - Token endpoint error shapes (non-200, 200 with non-JSON, network errors) - Response field handling (empty strings, numeric string coercion, zero/negative expires_in) - Extras handling (array limits, case-insensitive denylist, type coercion, tuple support, dict/set rejection) - Parameter wiring (presence/absence validation) - Confidential client requirements - Shared test utilities in conftest.py: - Fixtures: api_client_confidential, mock_discovery - Helpers: last_form(), last_auth_header(), mock_token_response(), token_success() - Assertions: assert_api_error(), assert_http_basic_auth(), assert_form_post() - Deterministic testing: freezegun for expires_at validation - Table-driven tests: Parameterized validation tests for all error conditions ## Documentation - Comprehensive README with usage examples, error handling, and warnings - Related SDKs section: Links to auth0-auth-js and auth0-api-js - Clear Early Access callout: Enterprise feature requiring tenant enablement - Subject token type namespace guidance: Avoid reserved OAuth namespaces - Extra parameters documentation: Reserved parameter warnings and array limits - Error handling examples: Both validation and API error scenarios ## Cross-SDK Validation Validated against auth0-auth-js for consistency: - Whitespace validation (strict, fail-fast) - Array size limits (DoS protection) - Reserved parameter checking (case-insensitive) - HTTP Basic authentication - Error handling patterns ## Follow-up Items - Poetry/requirements.txt: Consider auto-generating requirements.txt via poetry export in CI. Currently maintained manually for .github/workflows/sca_scan.yml (Snyk security scanning). Source of truth remains pyproject.toml + poetry.lock. ## Breaking Changes None. This is a new method with no impact on existing functionality. ## Related - RFC 8693: https://datatracker.ietf.org/doc/html/rfc8693 - Auth0 Docs: https://auth0.com/docs/authenticate/custom-token-exchange - Related SDK: https://github.com/auth0/auth0-auth-js, https://github.com/auth0/auth0-api-js 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a4c600c commit 5fb9754

11 files changed

Lines changed: 1466 additions & 225 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ setup.py
2222
test.py
2323
test-script.py
2424
.coverage
25-
coverage.xml
25+
coverage.xml
26+
27+
# IDE
28+
.idea/

README.md

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ This SDK provides comprehensive support for securing APIs with Auth0-issued acce
2828

2929
- [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
3030

31+
## Related SDKs
32+
33+
This library is part of Auth0's Python ecosystem for server-side authentication and API security. Related SDKs:
34+
35+
- **[auth0-auth-js](https://github.com/auth0/auth0-auth-js)** - JavaScript/TypeScript SDK for client-side authentication
36+
- **[auth0-api-js](https://github.com/auth0/auth0-api-js)** - JavaScript/TypeScript equivalent for server-side API security (the Node.js equivalent of this library)
37+
3138
## Getting Started
3239

3340
### 1. Install the SDK
@@ -113,6 +120,94 @@ asyncio.run(main())
113120

114121
More info https://auth0.com/docs/secure/tokens/token-vault
115122

123+
### 5. Custom Token Exchange (Early Access)
124+
125+
> [!NOTE]
126+
> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access) for Enterprise customers. Please reach out to Auth0 support to get it enabled for your tenant.
127+
128+
This feature requires a [confidential client](https://auth0.com/docs/get-started/applications/confidential-and-public-applications#confidential-applications) (both `client_id` and `client_secret` must be configured).
129+
130+
Custom Token Exchange allows you to exchange a subject token for Auth0 tokens using RFC 8693. This is useful for:
131+
- Getting Auth0 tokens for another audience
132+
- Integrating external identity providers
133+
- Migrating to Auth0
134+
135+
```python
136+
import asyncio
137+
138+
from auth0_api_python import ApiClient, ApiClientOptions
139+
140+
async def main():
141+
api_client = ApiClient(ApiClientOptions(
142+
domain="<AUTH0_DOMAIN>",
143+
audience="<AUTH0_AUDIENCE>",
144+
client_id="<AUTH0_CLIENT_ID>",
145+
client_secret="<AUTH0_CLIENT_SECRET>",
146+
timeout=10.0 # Optional: HTTP timeout in seconds (default: 10.0)
147+
))
148+
149+
subject_token = "..." # Token from your legacy system or external source
150+
151+
result = await api_client.get_token_by_exchange_profile(
152+
subject_token=subject_token,
153+
subject_token_type="urn:example:subject-token",
154+
audience="https://api.example.com", # Optional - omit if your Action or tenant configuration sets the audience
155+
scope="openid profile email", # Optional
156+
requested_token_type="urn:ietf:params:oauth:token-type:access_token" # Optional
157+
)
158+
159+
# Result contains access_token, expires_in, expires_at
160+
# id_token, refresh_token, and scope are profile/Action dependent (not guaranteed; scope may be empty)
161+
162+
asyncio.run(main())
163+
```
164+
165+
**Important:**
166+
- Client authentication is sent via HTTP Basic (`client_id`/`client_secret`), not in the form body.
167+
- The `subject_token_type` must match a Token Exchange Profile configured in Auth0. This URI identifies which profile will process the exchange and **must not use reserved OAuth namespaces (IETF or vendor-controlled)**. Use your own collision-resistant namespace. See the [Custom Token Exchange documentation](https://auth0.com/docs/authenticate/custom-token-exchange) for naming guidance.
168+
- If neither an explicit `audience` nor tenant/Action logic sets it, you may receive a token not targeted at your API.
169+
170+
#### Additional Parameters
171+
172+
You can pass additional parameters for your Token Exchange Profile or Actions via the `extra` parameter. These are sent as form fields to Auth0 and may be inspected by Actions:
173+
174+
```python
175+
result = await api_client.get_token_by_exchange_profile(
176+
subject_token=subject_token,
177+
subject_token_type="urn:example:subject-token",
178+
audience="https://api.example.com",
179+
extra={
180+
"device_id": "device-12345",
181+
"session_id": "sess-abc"
182+
}
183+
)
184+
```
185+
186+
> [!WARNING]
187+
> Extra parameters are sent as form fields and may appear in logs. Do not include secrets or sensitive data. Reserved OAuth parameter names (like `grant_type`, `client_id`, `scope`) cannot be used and will raise an error. Arrays are supported but limited to 20 values per key to prevent abuse.
188+
189+
#### Error Handling
190+
191+
```python
192+
from auth0_api_python import GetTokenByExchangeProfileError, ApiError
193+
194+
try:
195+
result = await api_client.get_token_by_exchange_profile(
196+
subject_token=subject_token,
197+
subject_token_type="urn:example:subject-token"
198+
)
199+
except GetTokenByExchangeProfileError as e:
200+
# Validation errors (invalid token format, missing credentials, reserved params, etc.)
201+
print(f"Validation error: {e}")
202+
except ApiError as e:
203+
# Token endpoint errors (invalid_grant, network issues, malformed responses, etc.)
204+
print(f"API error: {e.code} - {e.message} (status: {e.status_code})")
205+
```
206+
207+
**Related SDKs:** [auth0-auth-js](https://github.com/auth0/auth0-auth-js), [auth0-api-js](https://github.com/auth0/auth0-api-js) (JavaScript/TypeScript)
208+
209+
More info: https://auth0.com/docs/authenticate/custom-token-exchange
210+
116211
#### Requiring Additional Claims
117212

118213
If your application demands extra claims, specify them with `required_claims`:
@@ -126,7 +221,7 @@ decoded_and_verified_token = await api_client.verify_access_token(
126221

127222
If the token lacks `my_custom_claim` or fails any standard check (issuer mismatch, expired token, invalid signature), the method raises a `VerifyAccessTokenError`.
128223

129-
### 5. DPoP Authentication
224+
### 6. DPoP Authentication
130225

131226
> [!NOTE]
132227
> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant.

0 commit comments

Comments
 (0)