auth0-api-python 1.0.0b7__tar.gz → 1.0.0b9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auth0-api-python
3
- Version: 1.0.0b7
3
+ Version: 1.0.0b9
4
4
  Summary: SDK for verifying access tokens and securing APIs with Auth0, using Authlib.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -41,7 +41,8 @@ This SDK provides comprehensive support for securing APIs with Auth0-issued acce
41
41
 
42
42
  ### **Core Features**
43
43
  - **Unified Entry Point**: `verify_request()` - automatically detects and validates Bearer or DPoP schemes
44
- - **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS
44
+ - **Multi-Custom Domain (MCD)** - Accept tokens from multiple Auth0 domains with static lists or dynamic resolvers
45
+ - **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS with per-issuer caching
45
46
  - **JWT Validation** - Complete RS256 signature verification with claim validation
46
47
  - **DPoP Proof Verification** - Full RFC 9449 compliance with ES256 signature validation
47
48
  - **Flexible Configuration** - Support for both "Allowed" and "Required" DPoP modes
@@ -235,6 +236,92 @@ except ApiError as e:
235
236
 
236
237
  More info: https://auth0.com/docs/authenticate/custom-token-exchange
237
238
 
239
+ #### On Behalf Of Token Exchange
240
+
241
+ Use `get_token_on_behalf_of()` when your API receives an `Auth0` access token for itself and needs
242
+ to exchange it for another `Auth0` access token targeting a downstream API while preserving the
243
+ same user identity. This is especially useful for `MCP` servers and other intermediary APIs that
244
+ need to call downstream APIs on behalf of the user.
245
+
246
+ 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.
247
+
248
+ ```python
249
+ import httpx
250
+
251
+ async def handle_calendar_request(incoming_access_token: str):
252
+ await api_client.verify_access_token(access_token=incoming_access_token)
253
+
254
+ result = await api_client.get_token_on_behalf_of(
255
+ access_token=incoming_access_token,
256
+ audience="https://calendar-api.example.com",
257
+ scope="calendar:read calendar:write"
258
+ )
259
+
260
+ async with httpx.AsyncClient() as client:
261
+ downstream_response = await client.get(
262
+ "https://calendar-api.example.com/events",
263
+ headers={"Authorization": f"Bearer {result['access_token']}"}
264
+ )
265
+
266
+ downstream_response.raise_for_status()
267
+
268
+ return downstream_response.json()
269
+ ```
270
+
271
+ The OBO wrapper reuses the existing RFC 8693 exchange support and fixes both token-type parameters
272
+ to Auth0 access-token exchange. In the current implementation, the SDK forwards the incoming access
273
+ token as the `subject_token` and relies on Auth0 to handle any DPoP-specific behavior for that token.
274
+ The OBO result only includes access-token-oriented fields. It does not expose `id_token` or
275
+ `refresh_token`.
276
+
277
+ #### Inspecting Delegation After Token Verification
278
+
279
+ When a downstream API or `MCP` server receives an access token that may have been issued through
280
+ delegation, it can verify the token first and then inspect the `act` claim to identify the current
281
+ actor for authorization and the full delegation chain for logging or audit.
282
+
283
+ ```python
284
+ import logging
285
+
286
+ from auth0_api_python import (
287
+ ApiClient,
288
+ ApiClientOptions,
289
+ get_current_actor,
290
+ get_delegation_chain,
291
+ )
292
+
293
+ logger = logging.getLogger(__name__)
294
+
295
+ api_client = ApiClient(ApiClientOptions(
296
+ domain="<AUTH0_DOMAIN>",
297
+ audience="<AUTH0_AUDIENCE>",
298
+ ))
299
+
300
+ async def authorize_delegated_request(access_token: str):
301
+ claims = await api_client.verify_access_token(access_token=access_token)
302
+
303
+ current_actor = get_current_actor(claims)
304
+ delegation_chain = get_delegation_chain(claims)
305
+
306
+ if current_actor != "mcp_server_client_id":
307
+ raise PermissionError("unexpected actor")
308
+
309
+ logger.info(
310
+ "delegated request",
311
+ extra={
312
+ "user_sub": claims["sub"],
313
+ "current_actor": current_actor,
314
+ "delegation_chain": delegation_chain,
315
+ },
316
+ )
317
+
318
+ return claims
319
+ ```
320
+
321
+ Only the outermost `act.sub` represents the current actor and should be used for authorization
322
+ decisions. Nested `act` values represent prior actors in the delegation chain and are better suited
323
+ for logging, audit, or attribution.
324
+
238
325
  #### Requiring Additional Claims
239
326
 
240
327
  If your application demands extra claims, specify them with `required_claims`:
@@ -250,9 +337,6 @@ If the token lacks `my_custom_claim` or fails any standard check (issuer mismatc
250
337
 
251
338
  ### 6. DPoP Authentication
252
339
 
253
- > [!NOTE]
254
- > 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.
255
-
256
340
  This library supports **DPoP (Demonstrating Proof-of-Possession)** for enhanced security, allowing clients to prove possession of private keys bound to access tokens.
257
341
 
258
342
  #### Allowed Mode (Default)
@@ -303,6 +387,50 @@ api_client = ApiClient(ApiClientOptions(
303
387
  ))
304
388
  ```
305
389
 
390
+ ### 7. Multi-Custom Domain (MCD) Support
391
+
392
+ If your Auth0 tenant has multiple custom domains, or you're migrating between domains, the SDK can accept tokens from any of them:
393
+
394
+ #### Static Domain List
395
+
396
+ ```python
397
+ from auth0_api_python import ApiClient, ApiClientOptions
398
+
399
+ api_client = ApiClient(ApiClientOptions(
400
+ domains=[
401
+ "tenant.auth0.com",
402
+ "auth.example.com",
403
+ "auth.acme.org"
404
+ ],
405
+ audience="https://api.example.com"
406
+ ))
407
+
408
+ # Tokens from any of the three domains are accepted
409
+ claims = await api_client.verify_access_token(access_token)
410
+ ```
411
+
412
+ #### Dynamic Resolver
413
+
414
+ For runtime domain resolution based on request context:
415
+
416
+ ```python
417
+ from auth0_api_python import ApiClient, ApiClientOptions, DomainsResolverContext
418
+
419
+ def resolve_domains(context: DomainsResolverContext) -> list[str]:
420
+ # Determine allowed domains based on the request
421
+ return ["tenant.auth0.com", "auth.example.com"]
422
+
423
+ api_client = ApiClient(ApiClientOptions(
424
+ domains=resolve_domains,
425
+ audience="https://api.example.com"
426
+ ))
427
+ ```
428
+
429
+ For hybrid mode (migration scenarios), resolver patterns, error handling, and caching configuration, see the full guides:
430
+
431
+ - **[Multi-Custom Domain Guide](docs/MultipleCustomDomain.md)** - Configuration modes, resolver patterns, migration, error handling
432
+ - **[Caching Guide](docs/Caching.md)** - Cache tuning, custom adapters (Redis, Memcached)
433
+
306
434
  ## Feedback
307
435
 
308
436
  ### Contributing
@@ -336,3 +464,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
336
464
  <p align="center">
337
465
  This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-api-python/LICENSE"> LICENSE</a> file for more info.
338
466
  </p>
467
+
@@ -17,7 +17,8 @@ This SDK provides comprehensive support for securing APIs with Auth0-issued acce
17
17
 
18
18
  ### **Core Features**
19
19
  - **Unified Entry Point**: `verify_request()` - automatically detects and validates Bearer or DPoP schemes
20
- - **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS
20
+ - **Multi-Custom Domain (MCD)** - Accept tokens from multiple Auth0 domains with static lists or dynamic resolvers
21
+ - **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS with per-issuer caching
21
22
  - **JWT Validation** - Complete RS256 signature verification with claim validation
22
23
  - **DPoP Proof Verification** - Full RFC 9449 compliance with ES256 signature validation
23
24
  - **Flexible Configuration** - Support for both "Allowed" and "Required" DPoP modes
@@ -211,6 +212,92 @@ except ApiError as e:
211
212
 
212
213
  More info: https://auth0.com/docs/authenticate/custom-token-exchange
213
214
 
215
+ #### On Behalf Of Token Exchange
216
+
217
+ Use `get_token_on_behalf_of()` when your API receives an `Auth0` access token for itself and needs
218
+ to exchange it for another `Auth0` access token targeting a downstream API while preserving the
219
+ same user identity. This is especially useful for `MCP` servers and other intermediary APIs that
220
+ need to call downstream APIs on behalf of the user.
221
+
222
+ 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.
223
+
224
+ ```python
225
+ import httpx
226
+
227
+ async def handle_calendar_request(incoming_access_token: str):
228
+ await api_client.verify_access_token(access_token=incoming_access_token)
229
+
230
+ result = await api_client.get_token_on_behalf_of(
231
+ access_token=incoming_access_token,
232
+ audience="https://calendar-api.example.com",
233
+ scope="calendar:read calendar:write"
234
+ )
235
+
236
+ async with httpx.AsyncClient() as client:
237
+ downstream_response = await client.get(
238
+ "https://calendar-api.example.com/events",
239
+ headers={"Authorization": f"Bearer {result['access_token']}"}
240
+ )
241
+
242
+ downstream_response.raise_for_status()
243
+
244
+ return downstream_response.json()
245
+ ```
246
+
247
+ The OBO wrapper reuses the existing RFC 8693 exchange support and fixes both token-type parameters
248
+ to Auth0 access-token exchange. In the current implementation, the SDK forwards the incoming access
249
+ token as the `subject_token` and relies on Auth0 to handle any DPoP-specific behavior for that token.
250
+ The OBO result only includes access-token-oriented fields. It does not expose `id_token` or
251
+ `refresh_token`.
252
+
253
+ #### Inspecting Delegation After Token Verification
254
+
255
+ When a downstream API or `MCP` server receives an access token that may have been issued through
256
+ delegation, it can verify the token first and then inspect the `act` claim to identify the current
257
+ actor for authorization and the full delegation chain for logging or audit.
258
+
259
+ ```python
260
+ import logging
261
+
262
+ from auth0_api_python import (
263
+ ApiClient,
264
+ ApiClientOptions,
265
+ get_current_actor,
266
+ get_delegation_chain,
267
+ )
268
+
269
+ logger = logging.getLogger(__name__)
270
+
271
+ api_client = ApiClient(ApiClientOptions(
272
+ domain="<AUTH0_DOMAIN>",
273
+ audience="<AUTH0_AUDIENCE>",
274
+ ))
275
+
276
+ async def authorize_delegated_request(access_token: str):
277
+ claims = await api_client.verify_access_token(access_token=access_token)
278
+
279
+ current_actor = get_current_actor(claims)
280
+ delegation_chain = get_delegation_chain(claims)
281
+
282
+ if current_actor != "mcp_server_client_id":
283
+ raise PermissionError("unexpected actor")
284
+
285
+ logger.info(
286
+ "delegated request",
287
+ extra={
288
+ "user_sub": claims["sub"],
289
+ "current_actor": current_actor,
290
+ "delegation_chain": delegation_chain,
291
+ },
292
+ )
293
+
294
+ return claims
295
+ ```
296
+
297
+ Only the outermost `act.sub` represents the current actor and should be used for authorization
298
+ decisions. Nested `act` values represent prior actors in the delegation chain and are better suited
299
+ for logging, audit, or attribution.
300
+
214
301
  #### Requiring Additional Claims
215
302
 
216
303
  If your application demands extra claims, specify them with `required_claims`:
@@ -226,9 +313,6 @@ If the token lacks `my_custom_claim` or fails any standard check (issuer mismatc
226
313
 
227
314
  ### 6. DPoP Authentication
228
315
 
229
- > [!NOTE]
230
- > 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.
231
-
232
316
  This library supports **DPoP (Demonstrating Proof-of-Possession)** for enhanced security, allowing clients to prove possession of private keys bound to access tokens.
233
317
 
234
318
  #### Allowed Mode (Default)
@@ -279,6 +363,50 @@ api_client = ApiClient(ApiClientOptions(
279
363
  ))
280
364
  ```
281
365
 
366
+ ### 7. Multi-Custom Domain (MCD) Support
367
+
368
+ If your Auth0 tenant has multiple custom domains, or you're migrating between domains, the SDK can accept tokens from any of them:
369
+
370
+ #### Static Domain List
371
+
372
+ ```python
373
+ from auth0_api_python import ApiClient, ApiClientOptions
374
+
375
+ api_client = ApiClient(ApiClientOptions(
376
+ domains=[
377
+ "tenant.auth0.com",
378
+ "auth.example.com",
379
+ "auth.acme.org"
380
+ ],
381
+ audience="https://api.example.com"
382
+ ))
383
+
384
+ # Tokens from any of the three domains are accepted
385
+ claims = await api_client.verify_access_token(access_token)
386
+ ```
387
+
388
+ #### Dynamic Resolver
389
+
390
+ For runtime domain resolution based on request context:
391
+
392
+ ```python
393
+ from auth0_api_python import ApiClient, ApiClientOptions, DomainsResolverContext
394
+
395
+ def resolve_domains(context: DomainsResolverContext) -> list[str]:
396
+ # Determine allowed domains based on the request
397
+ return ["tenant.auth0.com", "auth.example.com"]
398
+
399
+ api_client = ApiClient(ApiClientOptions(
400
+ domains=resolve_domains,
401
+ audience="https://api.example.com"
402
+ ))
403
+ ```
404
+
405
+ For hybrid mode (migration scenarios), resolver patterns, error handling, and caching configuration, see the full guides:
406
+
407
+ - **[Multi-Custom Domain Guide](docs/MultipleCustomDomain.md)** - Configuration modes, resolver patterns, migration, error handling
408
+ - **[Caching Guide](docs/Caching.md)** - Cache tuning, custom adapters (Redis, Memcached)
409
+
282
410
  ## Feedback
283
411
 
284
412
  ### Contributing
@@ -311,4 +439,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
311
439
  </p>
312
440
  <p align="center">
313
441
  This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-api-python/LICENSE"> LICENSE</a> file for more info.
314
- </p>
442
+ </p>
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-api-python"
3
- version = "1.0.0.b7"
3
+ version = "1.0.0b9"
4
4
  description = "SDK for verifying access tokens and securing APIs with Auth0, using Authlib."
5
5
  authors = ["Auth0 <support@auth0.com>"]
6
6
  license = "MIT"
@@ -0,0 +1,38 @@
1
+ """
2
+ auth0-api-python
3
+
4
+ A lightweight Python SDK for verifying Auth0-issued access tokens
5
+ in server-side APIs, using Authlib for OIDC discovery and JWKS fetching.
6
+ """
7
+
8
+ from .act import get_current_actor, get_delegation_chain
9
+ from .api_client import ApiClient
10
+ from .cache import CacheAdapter, InMemoryCache
11
+ from .config import ApiClientOptions
12
+ from .errors import (
13
+ ApiError,
14
+ ConfigurationError,
15
+ DomainsResolverError,
16
+ GetTokenByExchangeProfileError,
17
+ )
18
+ from .types import (
19
+ DomainsResolver,
20
+ DomainsResolverContext,
21
+ OnBehalfOfTokenResult,
22
+ )
23
+
24
+ __all__ = [
25
+ "ApiClient",
26
+ "ApiClientOptions",
27
+ "ApiError",
28
+ "CacheAdapter",
29
+ "ConfigurationError",
30
+ "DomainsResolver",
31
+ "DomainsResolverContext",
32
+ "DomainsResolverError",
33
+ "GetTokenByExchangeProfileError",
34
+ "get_current_actor",
35
+ "get_delegation_chain",
36
+ "InMemoryCache",
37
+ "OnBehalfOfTokenResult",
38
+ ]
@@ -0,0 +1,64 @@
1
+ """
2
+ Helpers for working with the `act` claim on verified access token claims.
3
+ """
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any, Optional
7
+
8
+ from .errors import VerifyAccessTokenError
9
+
10
+ INVALID_ACT_CLAIM_MESSAGE = "Invalid act claim"
11
+
12
+
13
+ def get_current_actor(claims: Mapping[str, Any]) -> Optional[str]:
14
+ """
15
+ Return the current actor from the outermost `act.sub`, if present.
16
+
17
+ Only the outermost `act.sub` should be used for authorization decisions.
18
+ Nested `act` values represent prior actors and are informational.
19
+ """
20
+ if not isinstance(claims, Mapping):
21
+ raise VerifyAccessTokenError(INVALID_ACT_CLAIM_MESSAGE)
22
+
23
+ act_claim = claims.get("act")
24
+ if act_claim is None:
25
+ return None
26
+
27
+ if not isinstance(act_claim, Mapping):
28
+ raise VerifyAccessTokenError(INVALID_ACT_CLAIM_MESSAGE)
29
+
30
+ sub = act_claim.get("sub")
31
+ if not isinstance(sub, str) or not sub.strip():
32
+ raise VerifyAccessTokenError(INVALID_ACT_CLAIM_MESSAGE)
33
+
34
+ return sub
35
+
36
+
37
+ def get_delegation_chain(claims: Mapping[str, Any]) -> list[str]:
38
+ """
39
+ Return the delegation chain from newest actor to oldest actor.
40
+
41
+ The first entry is the current actor (outermost `act.sub`). Later entries are
42
+ prior actors from nested `act` values and are typically most useful for audit
43
+ and attribution.
44
+ """
45
+ if not isinstance(claims, Mapping):
46
+ raise VerifyAccessTokenError(INVALID_ACT_CLAIM_MESSAGE)
47
+
48
+ current = claims.get("act")
49
+ if current is None:
50
+ return []
51
+
52
+ chain: list[str] = []
53
+ while current is not None:
54
+ if not isinstance(current, Mapping):
55
+ raise VerifyAccessTokenError(INVALID_ACT_CLAIM_MESSAGE)
56
+
57
+ sub = current.get("sub")
58
+ if not isinstance(sub, str) or not sub.strip():
59
+ raise VerifyAccessTokenError(INVALID_ACT_CLAIM_MESSAGE)
60
+
61
+ chain.append(sub)
62
+ current = current.get("act")
63
+
64
+ return chain