auth0-api-python 1.0.0b9__tar.gz → 1.0.0b11__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.
Files changed (22) hide show
  1. auth0_api_python-1.0.0b9/README.md → auth0_api_python-1.0.0b11/PKG-INFO +94 -1
  2. auth0_api_python-1.0.0b9/PKG-INFO → auth0_api_python-1.0.0b11/README.md +68 -26
  3. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/pyproject.toml +5 -6
  4. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/__init__.py +26 -0
  5. auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/__init__.py +1 -0
  6. auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/cache_expiry.py +9 -0
  7. auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/cache_keys.py +147 -0
  8. auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/obo_cache.py +229 -0
  9. auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/token_vault_cache.py +72 -0
  10. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/act.py +2 -2
  11. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/api_client.py +393 -36
  12. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/cache.py +8 -8
  13. auth0_api_python-1.0.0b11/src/auth0_api_python/config.py +93 -0
  14. auth0_api_python-1.0.0b11/src/auth0_api_python/encryption.py +61 -0
  15. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/errors.py +28 -0
  16. auth0_api_python-1.0.0b11/src/auth0_api_python/telemetry.py +60 -0
  17. auth0_api_python-1.0.0b11/src/auth0_api_python/token_store.py +92 -0
  18. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/token_utils.py +9 -9
  19. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/types.py +24 -4
  20. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/utils.py +59 -15
  21. auth0_api_python-1.0.0b9/src/auth0_api_python/config.py +0 -66
  22. {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/LICENSE +0 -0
@@ -1,3 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: auth0-api-python
3
+ Version: 1.0.0b11
4
+ Summary: SDK for verifying access tokens and securing APIs with Auth0, using Authlib.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Auth0
8
+ Author-email: support@auth0.com
9
+ Requires-Python: >=3.10
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
+ Requires-Dist: ada-url (>=1.30.0,<2.0.0)
19
+ Requires-Dist: authlib (>=1.0,<2.0)
20
+ Requires-Dist: cryptography (>=43.0.1)
21
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
22
+ Requires-Dist: jwcrypto (>=1.5.7,<2.0.0)
23
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
24
+ Description-Content-Type: text/markdown
25
+
1
26
  The `auth0-api-python` library allows you to secure APIs running on Python, particularly for verifying Auth0-issued access tokens.
2
27
 
3
28
  It’s intended as a foundation for building more framework-specific integrations (e.g., with FastAPI, Django, etc.), but you can also use it directly in any Python server-side environment.
@@ -18,6 +43,7 @@ This SDK provides comprehensive support for securing APIs with Auth0-issued acce
18
43
  ### **Core Features**
19
44
  - **Unified Entry Point**: `verify_request()` - automatically detects and validates Bearer or DPoP schemes
20
45
  - **Multi-Custom Domain (MCD)** - Accept tokens from multiple Auth0 domains with static lists or dynamic resolvers
46
+ - **Organization Policy** - Enforce and optionally allowlist the `org_id` claim on incoming tokens
21
47
  - **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS with per-issuer caching
22
48
  - **JWT Validation** - Complete RS256 signature verification with claim validation
23
49
  - **DPoP Proof Verification** - Full RFC 9449 compliance with ES256 signature validation
@@ -42,7 +68,7 @@ This library is part of Auth0's Python ecosystem for server-side authentication
42
68
 
43
69
  ### 1. Install the SDK
44
70
 
45
- _This library requires Python 3.9+._
71
+ _This library requires Python 3.10+._
46
72
 
47
73
  ```shell
48
74
  pip install auth0-api-python
@@ -95,6 +121,12 @@ asyncio.run(main())
95
121
 
96
122
  In this example, the returned dictionary contains the decoded claims (like `sub`, `scope`, etc.) from the verified token.
97
123
 
124
+ If the OIDC discovery document or the JWKS request cannot be reached (connection error, timeout, or an HTTP `5xx` or `429` response), the method raises `AuthServerUnavailableError`, a subclass of `VerifyAccessTokenError` with status `503` and error code `auth_server_unavailable`. Other fetch failures, such as a `404` or a malformed response, still raise `VerifyAccessTokenError`. `verify_request()` raises the same error and sets no `WWW-Authenticate` header, so clients retry later instead of requesting a new token.
125
+
126
+ HTTP status codes are checked only for the default HTTP client. With `custom_fetch`, only a connection error or timeout raised by your function is reported as `AuthServerUnavailableError`.
127
+
128
+ This error comes from token verification only. Where verification is skipped, such as `get_client_credentials_token()` or an exchange with `verified=`, a failed discovery request raises the underlying `httpx` error.
129
+
98
130
  ### 4. Get an access token for a connection
99
131
 
100
132
  If you need to get an access token for an upstream idp via a connection, you can use the `get_access_token_for_connection` method:
@@ -114,6 +146,7 @@ async def main():
114
146
  connection = "my-connection" # The Auth0 connection to the upstream idp
115
147
  access_token = "..." # The Auth0 access token to exchange
116
148
 
149
+ await api_client.verify_access_token(access_token) # Verify first so the organization policy applies
117
150
  connection_access_token = await api_client.get_access_token_for_connection({"connection": connection, "access_token": access_token})
118
151
  # The returned token is the access token for the upstream idp
119
152
  print(connection_access_token)
@@ -121,6 +154,8 @@ async def main():
121
154
  asyncio.run(main())
122
155
  ```
123
156
 
157
+ When a `token_store` is configured on `ApiClientOptions`, the connection token is cached by caller, connection and `login_hint`, skipping the exchange on repeat calls. Pass `verified=` with a `VerifiedToken` built from the `verify_access_token()` claims to reuse that verification rather than verifying a second time. See [Access Token for a Connection](EXAMPLES.md#access-token-for-a-connection-token-vault) for a full example and setup requirements.
158
+
124
159
  More info https://auth0.com/docs/secure/tokens/token-vault
125
160
 
126
161
  ### 5. Custom Token Exchange (Early Access)
@@ -250,6 +285,25 @@ token as the `subject_token` and relies on Auth0 to handle any DPoP-specific beh
250
285
  The OBO result only includes access-token-oriented fields. It does not expose `id_token` or
251
286
  `refresh_token`.
252
287
 
288
+ Configuring a `token_store` on `ApiClientOptions` caches the exchanged token, so a repeat call for
289
+ the same caller, audience, organization, scopes, and session reuses it instead of exchanging again.
290
+ With no `token_store`, every call performs a fresh exchange, matching the existing behavior above.
291
+
292
+ #### Client Credentials for Server-to-Server Calls
293
+
294
+ Use `get_client_credentials_token()` to obtain an M2M access token for server-to-server calls
295
+ using the OAuth 2.0 client credentials grant.
296
+
297
+ ```python
298
+ result = await api_client.get_client_credentials_token(
299
+ audience="https://downstream-api.example.com",
300
+ scope="read:data",
301
+ )
302
+ # call downstream API with result["access_token"]
303
+ ```
304
+
305
+ See the **[Client Credentials example](EXAMPLES.md#client-credentials-for-server-to-server-calls)** for a full example that calls a downstream API, plus the caching behavior.
306
+
253
307
  #### Inspecting Delegation After Token Verification
254
308
 
255
309
  When a downstream API or `MCP` server receives an access token that may have been issued through
@@ -406,6 +460,44 @@ For hybrid mode (migration scenarios), resolver patterns, error handling, and ca
406
460
 
407
461
  - **[Multi-Custom Domain Guide](docs/MultipleCustomDomain.md)** - Configuration modes, resolver patterns, migration, error handling
408
462
  - **[Caching Guide](docs/Caching.md)** - Cache tuning, custom adapters (Redis, Memcached)
463
+ - **[Token Storage Guide](docs/TokenStorage.md)** - Caching OBO exchanges, custom TokenStore backends, at-rest encryption
464
+
465
+ ### 8. Anonymous Callers
466
+
467
+ [Anonymous Sessions](https://auth0.com/docs/manage-users/sessions/anonymous-sessions) give a visitor an Auth0 identity before they log in. Session creation happens in your web application. This SDK only validates the tokens they produce. The access token is a standard Auth0 Bearer JWT, so `verify_access_token()` and `verify_request()` validate it exactly like any other token. The only difference is the `sub` claim, which starts with `anon@`.
468
+
469
+ An anonymous token passes verification by default. Deciding whether an anonymous caller is authorized is your application's responsibility. See [Anonymous Callers](EXAMPLES.md#anonymous-callers) for allow, block-per-route, and block-globally patterns.
470
+
471
+ ### 9. Organization Policy
472
+
473
+ For APIs that need to enforce an Auth0 Organization context on every request, optionally restricted to a specific set of Organizations:
474
+
475
+ ```python
476
+ from auth0_api_python import ApiClient, ApiClientOptions
477
+
478
+ api_client = ApiClient(ApiClientOptions(
479
+ domain="tenant.auth0.com",
480
+ audience="https://api.example.com",
481
+ organization_policy="required",
482
+ organization_id=["org_abc123", "org_def456"]
483
+ ))
484
+
485
+ claims = await api_client.verify_access_token(access_token)
486
+ ```
487
+
488
+ See the **[Organization Policy Guide](docs/OrganizationPolicy.md)** for policy modes, the allowlist, and error handling.
489
+
490
+ ### 10. Client Telemetry
491
+
492
+ The SDK sends an `Auth0-Client` header and a `User-Agent` header (`Python/<version>`) on the requests it makes to Auth0 with its default HTTP client. Requests served by `custom_fetch` do not carry them. If you build a wrapper SDK on top of this one, pass `client_info` so the header names your SDK. An invalid `client_info` raises `ConfigurationError` when the client is created.
493
+
494
+ ```python
495
+ api_client = ApiClient(ApiClientOptions(
496
+ domain="<AUTH0_DOMAIN>",
497
+ audience="<AUTH0_AUDIENCE>",
498
+ client_info={"name": "my-wrapper-sdk", "version": "1.2.3", "env": {"my-framework": "4.5.6"}},
499
+ ))
500
+ ```
409
501
 
410
502
  ## Feedback
411
503
 
@@ -440,3 +532,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
440
532
  <p align="center">
441
533
  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.
442
534
  </p>
535
+
@@ -1,27 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: auth0-api-python
3
- Version: 1.0.0b9
4
- Summary: SDK for verifying access tokens and securing APIs with Auth0, using Authlib.
5
- License: MIT
6
- License-File: LICENSE
7
- Author: Auth0
8
- Author-email: support@auth0.com
9
- Requires-Python: >=3.9,<4.0
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
- Classifier: Programming Language :: Python :: 3.14
18
- Requires-Dist: ada-url (>=1.27.0,<2.0.0) ; python_version == "3.9"
19
- Requires-Dist: ada-url (>=1.30.0,<2.0.0) ; python_version >= "3.10"
20
- Requires-Dist: authlib (>=1.0,<2.0)
21
- Requires-Dist: httpx (>=0.28.1,<0.29.0)
22
- Requires-Dist: requests (>=2.31.0,<3.0.0)
23
- Description-Content-Type: text/markdown
24
-
25
1
  The `auth0-api-python` library allows you to secure APIs running on Python, particularly for verifying Auth0-issued access tokens.
26
2
 
27
3
  It’s intended as a foundation for building more framework-specific integrations (e.g., with FastAPI, Django, etc.), but you can also use it directly in any Python server-side environment.
@@ -42,6 +18,7 @@ This SDK provides comprehensive support for securing APIs with Auth0-issued acce
42
18
  ### **Core Features**
43
19
  - **Unified Entry Point**: `verify_request()` - automatically detects and validates Bearer or DPoP schemes
44
20
  - **Multi-Custom Domain (MCD)** - Accept tokens from multiple Auth0 domains with static lists or dynamic resolvers
21
+ - **Organization Policy** - Enforce and optionally allowlist the `org_id` claim on incoming tokens
45
22
  - **OIDC Discovery** - Automatic fetching of Auth0 metadata and JWKS with per-issuer caching
46
23
  - **JWT Validation** - Complete RS256 signature verification with claim validation
47
24
  - **DPoP Proof Verification** - Full RFC 9449 compliance with ES256 signature validation
@@ -66,7 +43,7 @@ This library is part of Auth0's Python ecosystem for server-side authentication
66
43
 
67
44
  ### 1. Install the SDK
68
45
 
69
- _This library requires Python 3.9+._
46
+ _This library requires Python 3.10+._
70
47
 
71
48
  ```shell
72
49
  pip install auth0-api-python
@@ -119,6 +96,12 @@ asyncio.run(main())
119
96
 
120
97
  In this example, the returned dictionary contains the decoded claims (like `sub`, `scope`, etc.) from the verified token.
121
98
 
99
+ If the OIDC discovery document or the JWKS request cannot be reached (connection error, timeout, or an HTTP `5xx` or `429` response), the method raises `AuthServerUnavailableError`, a subclass of `VerifyAccessTokenError` with status `503` and error code `auth_server_unavailable`. Other fetch failures, such as a `404` or a malformed response, still raise `VerifyAccessTokenError`. `verify_request()` raises the same error and sets no `WWW-Authenticate` header, so clients retry later instead of requesting a new token.
100
+
101
+ HTTP status codes are checked only for the default HTTP client. With `custom_fetch`, only a connection error or timeout raised by your function is reported as `AuthServerUnavailableError`.
102
+
103
+ This error comes from token verification only. Where verification is skipped, such as `get_client_credentials_token()` or an exchange with `verified=`, a failed discovery request raises the underlying `httpx` error.
104
+
122
105
  ### 4. Get an access token for a connection
123
106
 
124
107
  If you need to get an access token for an upstream idp via a connection, you can use the `get_access_token_for_connection` method:
@@ -138,6 +121,7 @@ async def main():
138
121
  connection = "my-connection" # The Auth0 connection to the upstream idp
139
122
  access_token = "..." # The Auth0 access token to exchange
140
123
 
124
+ await api_client.verify_access_token(access_token) # Verify first so the organization policy applies
141
125
  connection_access_token = await api_client.get_access_token_for_connection({"connection": connection, "access_token": access_token})
142
126
  # The returned token is the access token for the upstream idp
143
127
  print(connection_access_token)
@@ -145,6 +129,8 @@ async def main():
145
129
  asyncio.run(main())
146
130
  ```
147
131
 
132
+ When a `token_store` is configured on `ApiClientOptions`, the connection token is cached by caller, connection and `login_hint`, skipping the exchange on repeat calls. Pass `verified=` with a `VerifiedToken` built from the `verify_access_token()` claims to reuse that verification rather than verifying a second time. See [Access Token for a Connection](EXAMPLES.md#access-token-for-a-connection-token-vault) for a full example and setup requirements.
133
+
148
134
  More info https://auth0.com/docs/secure/tokens/token-vault
149
135
 
150
136
  ### 5. Custom Token Exchange (Early Access)
@@ -274,6 +260,25 @@ token as the `subject_token` and relies on Auth0 to handle any DPoP-specific beh
274
260
  The OBO result only includes access-token-oriented fields. It does not expose `id_token` or
275
261
  `refresh_token`.
276
262
 
263
+ Configuring a `token_store` on `ApiClientOptions` caches the exchanged token, so a repeat call for
264
+ the same caller, audience, organization, scopes, and session reuses it instead of exchanging again.
265
+ With no `token_store`, every call performs a fresh exchange, matching the existing behavior above.
266
+
267
+ #### Client Credentials for Server-to-Server Calls
268
+
269
+ Use `get_client_credentials_token()` to obtain an M2M access token for server-to-server calls
270
+ using the OAuth 2.0 client credentials grant.
271
+
272
+ ```python
273
+ result = await api_client.get_client_credentials_token(
274
+ audience="https://downstream-api.example.com",
275
+ scope="read:data",
276
+ )
277
+ # call downstream API with result["access_token"]
278
+ ```
279
+
280
+ See the **[Client Credentials example](EXAMPLES.md#client-credentials-for-server-to-server-calls)** for a full example that calls a downstream API, plus the caching behavior.
281
+
277
282
  #### Inspecting Delegation After Token Verification
278
283
 
279
284
  When a downstream API or `MCP` server receives an access token that may have been issued through
@@ -430,6 +435,44 @@ For hybrid mode (migration scenarios), resolver patterns, error handling, and ca
430
435
 
431
436
  - **[Multi-Custom Domain Guide](docs/MultipleCustomDomain.md)** - Configuration modes, resolver patterns, migration, error handling
432
437
  - **[Caching Guide](docs/Caching.md)** - Cache tuning, custom adapters (Redis, Memcached)
438
+ - **[Token Storage Guide](docs/TokenStorage.md)** - Caching OBO exchanges, custom TokenStore backends, at-rest encryption
439
+
440
+ ### 8. Anonymous Callers
441
+
442
+ [Anonymous Sessions](https://auth0.com/docs/manage-users/sessions/anonymous-sessions) give a visitor an Auth0 identity before they log in. Session creation happens in your web application. This SDK only validates the tokens they produce. The access token is a standard Auth0 Bearer JWT, so `verify_access_token()` and `verify_request()` validate it exactly like any other token. The only difference is the `sub` claim, which starts with `anon@`.
443
+
444
+ An anonymous token passes verification by default. Deciding whether an anonymous caller is authorized is your application's responsibility. See [Anonymous Callers](EXAMPLES.md#anonymous-callers) for allow, block-per-route, and block-globally patterns.
445
+
446
+ ### 9. Organization Policy
447
+
448
+ For APIs that need to enforce an Auth0 Organization context on every request, optionally restricted to a specific set of Organizations:
449
+
450
+ ```python
451
+ from auth0_api_python import ApiClient, ApiClientOptions
452
+
453
+ api_client = ApiClient(ApiClientOptions(
454
+ domain="tenant.auth0.com",
455
+ audience="https://api.example.com",
456
+ organization_policy="required",
457
+ organization_id=["org_abc123", "org_def456"]
458
+ ))
459
+
460
+ claims = await api_client.verify_access_token(access_token)
461
+ ```
462
+
463
+ See the **[Organization Policy Guide](docs/OrganizationPolicy.md)** for policy modes, the allowlist, and error handling.
464
+
465
+ ### 10. Client Telemetry
466
+
467
+ The SDK sends an `Auth0-Client` header and a `User-Agent` header (`Python/<version>`) on the requests it makes to Auth0 with its default HTTP client. Requests served by `custom_fetch` do not carry them. If you build a wrapper SDK on top of this one, pass `client_info` so the header names your SDK. An invalid `client_info` raises `ConfigurationError` when the client is created.
468
+
469
+ ```python
470
+ api_client = ApiClient(ApiClientOptions(
471
+ domain="<AUTH0_DOMAIN>",
472
+ audience="<AUTH0_AUDIENCE>",
473
+ client_info={"name": "my-wrapper-sdk", "version": "1.2.3", "env": {"my-framework": "4.5.6"}},
474
+ ))
475
+ ```
433
476
 
434
477
  ## Feedback
435
478
 
@@ -464,4 +507,3 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
464
507
  <p align="center">
465
508
  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.
466
509
  </p>
467
-
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-api-python"
3
- version = "1.0.0b9"
3
+ version = "1.0.0b11"
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"
@@ -11,14 +11,13 @@ packages = [
11
11
  ]
12
12
 
13
13
  [tool.poetry.dependencies]
14
- python = "^3.9"
14
+ python = ">=3.10"
15
15
  authlib = "^1.0" # For JWT/OIDC features
16
16
  requests = "^2.31.0" # If you use requests for HTTP calls (e.g., discovery)
17
17
  httpx = "^0.28.1"
18
- ada-url = [
19
- {version = "^1.30.0", python = ">=3.10"},
20
- {version = "^1.27.0", python = ">=3.9,<3.10"}
21
- ]
18
+ cryptography = ">=43.0.1"
19
+ jwcrypto = "^1.5.7"
20
+ ada-url = "^1.30.0"
22
21
 
23
22
  [tool.poetry.group.dev.dependencies]
24
23
  pytest = "^8.0"
@@ -11,11 +11,25 @@ from .cache import CacheAdapter, InMemoryCache
11
11
  from .config import ApiClientOptions
12
12
  from .errors import (
13
13
  ApiError,
14
+ AuthServerUnavailableError,
14
15
  ConfigurationError,
15
16
  DomainsResolverError,
17
+ GetAccessTokenForConnectionError,
18
+ GetClientCredentialsTokenError,
16
19
  GetTokenByExchangeProfileError,
20
+ MissingOrganizationError,
21
+ OrganizationNotAllowedError,
22
+ VerifyAccessTokenError,
23
+ )
24
+ from .token_store import (
25
+ AbstractTokenStore,
26
+ IndexedTokenStore,
27
+ TokenIndexMember,
28
+ TokenSet,
29
+ VerifiedToken,
17
30
  )
18
31
  from .types import (
32
+ ClientCredentialsTokenResult,
19
33
  DomainsResolver,
20
34
  DomainsResolverContext,
21
35
  OnBehalfOfTokenResult,
@@ -25,14 +39,26 @@ __all__ = [
25
39
  "ApiClient",
26
40
  "ApiClientOptions",
27
41
  "ApiError",
42
+ "AuthServerUnavailableError",
28
43
  "CacheAdapter",
44
+ "ClientCredentialsTokenResult",
29
45
  "ConfigurationError",
30
46
  "DomainsResolver",
31
47
  "DomainsResolverContext",
32
48
  "DomainsResolverError",
49
+ "GetAccessTokenForConnectionError",
50
+ "GetClientCredentialsTokenError",
33
51
  "GetTokenByExchangeProfileError",
52
+ "VerifyAccessTokenError",
34
53
  "get_current_actor",
35
54
  "get_delegation_chain",
36
55
  "InMemoryCache",
56
+ "MissingOrganizationError",
57
+ "AbstractTokenStore",
58
+ "IndexedTokenStore",
59
+ "TokenIndexMember",
60
+ "VerifiedToken",
37
61
  "OnBehalfOfTokenResult",
62
+ "OrganizationNotAllowedError",
63
+ "TokenSet",
38
64
  ]
@@ -0,0 +1 @@
1
+ """Internal machinery for auth0-api-python. Not part of the public API."""
@@ -0,0 +1,9 @@
1
+ """Freshness rule shared by every cache of a token the SDK minted."""
2
+
3
+ # A token this close to expiry is treated as expired, so it is unlikely to lapse before the caller uses it.
4
+ EXPIRY_BUFFER_SECONDS = 30
5
+
6
+
7
+ def is_fresh(expires_at: int, now: int) -> bool:
8
+ """True when a cached token still has more than the expiry buffer left."""
9
+ return expires_at - EXPIRY_BUFFER_SECONDS > now
@@ -0,0 +1,147 @@
1
+ """Cache-key builders and scope helpers for the tokens the SDK mints."""
2
+
3
+ import hashlib
4
+
5
+ from ..utils import get_unverified_payload
6
+
7
+ _FIELD_SEPARATOR = "\x1f"
8
+
9
+
10
+ def _normalized_scopes(scope: str | None) -> str:
11
+ """Sort and dedupe scopes so equivalent scope strings produce the same cache key."""
12
+ if not scope:
13
+ return ""
14
+ return " ".join(sorted(set(scope.split())))
15
+
16
+
17
+ def is_covered_by(requested: str | None, granted: str | None) -> bool:
18
+ """True when the requested scopes are all covered by the granted scopes."""
19
+ granted_set = set(_normalized_scopes(granted).split())
20
+ requested_set = set(_normalized_scopes(requested).split())
21
+ return requested_set.issubset(granted_set)
22
+
23
+
24
+ def session_fingerprint(access_token: str) -> str:
25
+ """
26
+ Derive a session-scoped fingerprint for an access token, to keep concurrent
27
+ sessions' cached tokens from colliding. Not a trust or authorization signal:
28
+ the token is decoded without signature verification.
29
+ """
30
+ try:
31
+ payload = get_unverified_payload(access_token)
32
+ except ValueError:
33
+ return hashlib.sha256(access_token.encode()).hexdigest()
34
+
35
+ sid = payload.get("sid")
36
+ if isinstance(sid, str) and sid:
37
+ return sid
38
+
39
+ jti = payload.get("jti")
40
+ if isinstance(jti, str) and jti:
41
+ return jti
42
+
43
+ return hashlib.sha256(access_token.encode()).hexdigest()
44
+
45
+
46
+ def _obo_identity_fields(
47
+ namespace: str,
48
+ *,
49
+ issuer: str,
50
+ incoming_client_id: str,
51
+ exchange_tenant: str,
52
+ exchange_client_id: str,
53
+ audience: str,
54
+ org_id: str | None,
55
+ session_key: str,
56
+ ) -> list[str]:
57
+ """The fields every OBO cache key shares, namespaced by key kind.
58
+
59
+ Folds in the verified issuer, the incoming token's client, the exchange tenant, and the
60
+ exchange client so a token minted under one of them is never read back under another.
61
+ """
62
+ return [
63
+ namespace,
64
+ issuer,
65
+ incoming_client_id,
66
+ exchange_tenant,
67
+ exchange_client_id,
68
+ audience,
69
+ org_id or "",
70
+ session_key,
71
+ ]
72
+
73
+
74
+ def obo_cache_key(
75
+ *,
76
+ layout: str,
77
+ sub: str,
78
+ issuer: str,
79
+ incoming_client_id: str,
80
+ exchange_tenant: str,
81
+ exchange_client_id: str,
82
+ audience: str,
83
+ org_id: str | None,
84
+ scopes: str | None,
85
+ session_key: str,
86
+ ) -> str:
87
+ """Token cache key for an On Behalf Of exchange.
88
+
89
+ layout is "strict" or "index" so the two scope-matching modes never read each other's tokens.
90
+ The normalized scopes are folded in.
91
+ """
92
+ fields = _obo_identity_fields(
93
+ "obo_token_" + layout,
94
+ issuer=issuer,
95
+ incoming_client_id=incoming_client_id,
96
+ exchange_tenant=exchange_tenant,
97
+ exchange_client_id=exchange_client_id,
98
+ audience=audience,
99
+ org_id=org_id,
100
+ session_key=session_key,
101
+ )
102
+ fields.append(_normalized_scopes(scopes))
103
+ return sub + ":" + hashlib.sha256(_FIELD_SEPARATOR.join(fields).encode()).hexdigest()
104
+
105
+
106
+ def index_cache_key(
107
+ *,
108
+ sub: str,
109
+ issuer: str,
110
+ incoming_client_id: str,
111
+ exchange_tenant: str,
112
+ exchange_client_id: str,
113
+ audience: str,
114
+ org_id: str | None,
115
+ session_key: str,
116
+ ) -> str:
117
+ """Cache key for the OBO token index, scoped to caller, issuer, exchange, audience, org, and session.
118
+
119
+ The "obo_index" namespace keeps it from colliding with either layout's token cache key.
120
+ """
121
+ fields = _obo_identity_fields(
122
+ "obo_index",
123
+ issuer=issuer,
124
+ incoming_client_id=incoming_client_id,
125
+ exchange_tenant=exchange_tenant,
126
+ exchange_client_id=exchange_client_id,
127
+ audience=audience,
128
+ org_id=org_id,
129
+ session_key=session_key,
130
+ )
131
+ return sub + ":" + hashlib.sha256(_FIELD_SEPARATOR.join(fields).encode()).hexdigest()
132
+
133
+
134
+ def m2m_cache_key(*, tenant: str, client_id: str, audience: str, scopes: str | None) -> str:
135
+ """Cache key for a client-credentials (M2M) exchange, scoped to tenant, client, audience, and scopes."""
136
+ fields = _FIELD_SEPARATOR.join(
137
+ ["m2m", tenant, client_id, audience, _normalized_scopes(scopes)]
138
+ )
139
+ return hashlib.sha256(fields.encode()).hexdigest()
140
+
141
+
142
+ def token_vault_cache_key(
143
+ *, sub: str, connection: str, tenant: str, client_id: str, login_hint: str | None = None
144
+ ) -> str:
145
+ """Cache key for a Token Vault exchange, scoped to tenant, client, caller, connection, and login hint."""
146
+ fields = _FIELD_SEPARATOR.join(["token_vault", tenant, client_id, sub, connection, login_hint or ""])
147
+ return hashlib.sha256(fields.encode()).hexdigest()