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.
- auth0_api_python-1.0.0b9/README.md → auth0_api_python-1.0.0b11/PKG-INFO +94 -1
- auth0_api_python-1.0.0b9/PKG-INFO → auth0_api_python-1.0.0b11/README.md +68 -26
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/pyproject.toml +5 -6
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/__init__.py +26 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/__init__.py +1 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/cache_expiry.py +9 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/cache_keys.py +147 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/obo_cache.py +229 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/_internal/token_vault_cache.py +72 -0
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/act.py +2 -2
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/api_client.py +393 -36
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/cache.py +8 -8
- auth0_api_python-1.0.0b11/src/auth0_api_python/config.py +93 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/encryption.py +61 -0
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/errors.py +28 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/telemetry.py +60 -0
- auth0_api_python-1.0.0b11/src/auth0_api_python/token_store.py +92 -0
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/token_utils.py +9 -9
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/types.py +24 -4
- {auth0_api_python-1.0.0b9 → auth0_api_python-1.0.0b11}/src/auth0_api_python/utils.py +59 -15
- auth0_api_python-1.0.0b9/src/auth0_api_python/config.py +0 -66
- {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.
|
|
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.
|
|
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.
|
|
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 = "
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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()
|