auth0-api-python 1.0.0b4__tar.gz → 1.0.0b5__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.0b4 → auth0_api_python-1.0.0b5}/PKG-INFO +30 -2
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/README.md +29 -1
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/pyproject.toml +1 -1
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/src/auth0_api_python/api_client.py +111 -0
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/src/auth0_api_python/config.py +14 -8
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/src/auth0_api_python/errors.py +36 -0
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/LICENSE +0 -0
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/src/auth0_api_python/__init__.py +0 -0
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/src/auth0_api_python/token_utils.py +0 -0
- {auth0_api_python-1.0.0b4 → auth0_api_python-1.0.0b5}/src/auth0_api_python/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: auth0-api-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b5
|
|
4
4
|
Summary: SDK for verifying access tokens and securing APIs with Auth0, using Authlib.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Auth0
|
|
@@ -106,6 +106,34 @@ asyncio.run(main())
|
|
|
106
106
|
|
|
107
107
|
In this example, the returned dictionary contains the decoded claims (like `sub`, `scope`, etc.) from the verified token.
|
|
108
108
|
|
|
109
|
+
### 4. Get an access token for a connection
|
|
110
|
+
|
|
111
|
+
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:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import asyncio
|
|
115
|
+
|
|
116
|
+
from auth0_api_python import ApiClient, ApiClientOptions
|
|
117
|
+
|
|
118
|
+
async def main():
|
|
119
|
+
api_client = ApiClient(ApiClientOptions(
|
|
120
|
+
domain="<AUTH0_DOMAIN>",
|
|
121
|
+
audience="<AUTH0_AUDIENCE>",
|
|
122
|
+
client_id="<AUTH0_CLIENT_ID>",
|
|
123
|
+
client_secret="<AUTH0_CLIENT_SECRET>",
|
|
124
|
+
))
|
|
125
|
+
connection = "my-connection" # The Auth0 connection to the upstream idp
|
|
126
|
+
access_token = "..." # The Auth0 access token to exchange
|
|
127
|
+
|
|
128
|
+
connection_access_token = await api_client.get_access_token_for_connection({"connection": connection, "access_token": access_token})
|
|
129
|
+
# The returned token is the access token for the upstream idp
|
|
130
|
+
print(connection_access_token)
|
|
131
|
+
|
|
132
|
+
asyncio.run(main())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
More info https://auth0.com/docs/secure/tokens/token-vault
|
|
136
|
+
|
|
109
137
|
#### Requiring Additional Claims
|
|
110
138
|
|
|
111
139
|
If your application demands extra claims, specify them with `required_claims`:
|
|
@@ -119,7 +147,7 @@ decoded_and_verified_token = await api_client.verify_access_token(
|
|
|
119
147
|
|
|
120
148
|
If the token lacks `my_custom_claim` or fails any standard check (issuer mismatch, expired token, invalid signature), the method raises a `VerifyAccessTokenError`.
|
|
121
149
|
|
|
122
|
-
###
|
|
150
|
+
### 5. DPoP Authentication
|
|
123
151
|
|
|
124
152
|
> [!NOTE]
|
|
125
153
|
> 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.
|
|
@@ -85,6 +85,34 @@ asyncio.run(main())
|
|
|
85
85
|
|
|
86
86
|
In this example, the returned dictionary contains the decoded claims (like `sub`, `scope`, etc.) from the verified token.
|
|
87
87
|
|
|
88
|
+
### 4. Get an access token for a connection
|
|
89
|
+
|
|
90
|
+
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:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import asyncio
|
|
94
|
+
|
|
95
|
+
from auth0_api_python import ApiClient, ApiClientOptions
|
|
96
|
+
|
|
97
|
+
async def main():
|
|
98
|
+
api_client = ApiClient(ApiClientOptions(
|
|
99
|
+
domain="<AUTH0_DOMAIN>",
|
|
100
|
+
audience="<AUTH0_AUDIENCE>",
|
|
101
|
+
client_id="<AUTH0_CLIENT_ID>",
|
|
102
|
+
client_secret="<AUTH0_CLIENT_SECRET>",
|
|
103
|
+
))
|
|
104
|
+
connection = "my-connection" # The Auth0 connection to the upstream idp
|
|
105
|
+
access_token = "..." # The Auth0 access token to exchange
|
|
106
|
+
|
|
107
|
+
connection_access_token = await api_client.get_access_token_for_connection({"connection": connection, "access_token": access_token})
|
|
108
|
+
# The returned token is the access token for the upstream idp
|
|
109
|
+
print(connection_access_token)
|
|
110
|
+
|
|
111
|
+
asyncio.run(main())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
More info https://auth0.com/docs/secure/tokens/token-vault
|
|
115
|
+
|
|
88
116
|
#### Requiring Additional Claims
|
|
89
117
|
|
|
90
118
|
If your application demands extra claims, specify them with `required_claims`:
|
|
@@ -98,7 +126,7 @@ decoded_and_verified_token = await api_client.verify_access_token(
|
|
|
98
126
|
|
|
99
127
|
If the token lacks `my_custom_claim` or fails any standard check (issuer mismatch, expired token, invalid signature), the method raises a `VerifyAccessTokenError`.
|
|
100
128
|
|
|
101
|
-
###
|
|
129
|
+
### 5. DPoP Authentication
|
|
102
130
|
|
|
103
131
|
> [!NOTE]
|
|
104
132
|
> 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.
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from typing import Any, Optional
|
|
3
3
|
|
|
4
|
+
import httpx
|
|
4
5
|
from authlib.jose import JsonWebKey, JsonWebToken
|
|
5
6
|
|
|
6
7
|
from .config import ApiClientOptions
|
|
7
8
|
from .errors import (
|
|
9
|
+
ApiError,
|
|
8
10
|
BaseAuthError,
|
|
11
|
+
GetAccessTokenForConnectionError,
|
|
9
12
|
InvalidAuthSchemeError,
|
|
10
13
|
InvalidDpopProofError,
|
|
11
14
|
MissingAuthorizationError,
|
|
@@ -390,6 +393,114 @@ class ApiClient:
|
|
|
390
393
|
|
|
391
394
|
return claims
|
|
392
395
|
|
|
396
|
+
async def get_access_token_for_connection(self, options: dict[str, Any]) -> dict[str, Any]:
|
|
397
|
+
"""
|
|
398
|
+
Retrieves a token for a connection.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
options: Options for retrieving an access token for a connection.
|
|
402
|
+
Must include 'connection' and 'access_token' keys.
|
|
403
|
+
May optionally include 'login_hint'.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
GetAccessTokenForConnectionError: If there was an issue requesting the access token.
|
|
407
|
+
ApiError: If the token exchange endpoint returns an error.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dictionary containing the token response with access_token, expires_in, and scope.
|
|
411
|
+
"""
|
|
412
|
+
# Constants
|
|
413
|
+
SUBJECT_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token" # noqa S105
|
|
414
|
+
REQUESTED_TOKEN_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = "http://auth0.com/oauth/token-type/federated-connection-access-token" # noqa S105
|
|
415
|
+
GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token" # noqa S105
|
|
416
|
+
connection = options.get("connection")
|
|
417
|
+
access_token = options.get("access_token")
|
|
418
|
+
|
|
419
|
+
if not connection:
|
|
420
|
+
raise MissingRequiredArgumentError("connection")
|
|
421
|
+
|
|
422
|
+
if not access_token:
|
|
423
|
+
raise MissingRequiredArgumentError("access_token")
|
|
424
|
+
|
|
425
|
+
client_id = self.options.client_id
|
|
426
|
+
client_secret = self.options.client_secret
|
|
427
|
+
if not client_id or not client_secret:
|
|
428
|
+
raise GetAccessTokenForConnectionError("You must configure the SDK with a client_id and client_secret to use get_access_token_for_connection.")
|
|
429
|
+
|
|
430
|
+
metadata = await self._discover()
|
|
431
|
+
|
|
432
|
+
token_endpoint = metadata.get("token_endpoint")
|
|
433
|
+
if not token_endpoint:
|
|
434
|
+
raise GetAccessTokenForConnectionError("Token endpoint missing in OIDC metadata")
|
|
435
|
+
|
|
436
|
+
# Prepare parameters
|
|
437
|
+
params = {
|
|
438
|
+
"connection": connection,
|
|
439
|
+
"requested_token_type": REQUESTED_TOKEN_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN,
|
|
440
|
+
"grant_type": GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN,
|
|
441
|
+
"client_id": client_id,
|
|
442
|
+
"subject_token": access_token,
|
|
443
|
+
"subject_token_type": SUBJECT_TYPE_ACCESS_TOKEN,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
# Add login_hint if provided
|
|
447
|
+
if "login_hint" in options and options["login_hint"]:
|
|
448
|
+
params["login_hint"] = options["login_hint"]
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
async with httpx.AsyncClient() as client:
|
|
452
|
+
response = await client.post(
|
|
453
|
+
token_endpoint,
|
|
454
|
+
data=params,
|
|
455
|
+
auth=(client_id, client_secret)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if response.status_code != 200:
|
|
459
|
+
error_data = response.json() if "json" in response.headers.get(
|
|
460
|
+
"content-type", "").lower() else {}
|
|
461
|
+
raise ApiError(
|
|
462
|
+
error_data.get("error", "connection_token_error"),
|
|
463
|
+
error_data.get(
|
|
464
|
+
"error_description", f"Failed to get token for connection: {response.status_code}"),
|
|
465
|
+
response.status_code
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
token_endpoint_response = response.json()
|
|
470
|
+
except Exception:
|
|
471
|
+
raise ApiError("invalid_json", "Token endpoint returned invalid JSON.")
|
|
472
|
+
|
|
473
|
+
access_token = token_endpoint_response.get("access_token")
|
|
474
|
+
if not isinstance(access_token, str) or not access_token:
|
|
475
|
+
raise ApiError("invalid_response", "Missing or invalid access_token in response.", 502)
|
|
476
|
+
|
|
477
|
+
expires_in_raw = token_endpoint_response.get("expires_in", 3600)
|
|
478
|
+
try:
|
|
479
|
+
expires_in = int(expires_in_raw)
|
|
480
|
+
except (TypeError, ValueError):
|
|
481
|
+
raise ApiError("invalid_response", "expires_in is not an integer.", 502)
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
"access_token": access_token,
|
|
485
|
+
"expires_at": int(time.time()) + expires_in,
|
|
486
|
+
"scope": token_endpoint_response.get("scope", "")
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
except httpx.TimeoutException as exc:
|
|
490
|
+
raise ApiError(
|
|
491
|
+
"timeout_error",
|
|
492
|
+
f"Request to token endpoint timed out: {str(exc)}",
|
|
493
|
+
504,
|
|
494
|
+
exc
|
|
495
|
+
)
|
|
496
|
+
except httpx.HTTPError as exc:
|
|
497
|
+
raise ApiError(
|
|
498
|
+
"network_error",
|
|
499
|
+
f"Network error occurred: {str(exc)}",
|
|
500
|
+
502,
|
|
501
|
+
exc
|
|
502
|
+
)
|
|
503
|
+
|
|
393
504
|
# ===== Private Methods =====
|
|
394
505
|
|
|
395
506
|
async def _discover(self) -> dict[str, Any]:
|
|
@@ -17,16 +17,20 @@ class ApiClientOptions:
|
|
|
17
17
|
dpop_required: Whether DPoP is required (default: False, allows both Bearer and DPoP).
|
|
18
18
|
dpop_iat_leeway: Leeway in seconds for DPoP proof iat claim (default: 30).
|
|
19
19
|
dpop_iat_offset: Maximum age in seconds for DPoP proof iat claim (default: 300).
|
|
20
|
+
client_id: Optional required if you want to use get_access_token_for_connection.
|
|
21
|
+
client_secret: Optional required if you want to use get_access_token_for_connection.
|
|
20
22
|
"""
|
|
21
23
|
def __init__(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
self,
|
|
25
|
+
domain: str,
|
|
26
|
+
audience: str,
|
|
27
|
+
custom_fetch: Optional[Callable[..., object]] = None,
|
|
28
|
+
dpop_enabled: bool = True,
|
|
29
|
+
dpop_required: bool = False,
|
|
30
|
+
dpop_iat_leeway: int = 30,
|
|
31
|
+
dpop_iat_offset: int = 300,
|
|
32
|
+
client_id: Optional[str] = None,
|
|
33
|
+
client_secret: Optional[str] = None,
|
|
30
34
|
):
|
|
31
35
|
self.domain = domain
|
|
32
36
|
self.audience = audience
|
|
@@ -35,3 +39,5 @@ class ApiClientOptions:
|
|
|
35
39
|
self.dpop_required = dpop_required
|
|
36
40
|
self.dpop_iat_leeway = dpop_iat_leeway
|
|
37
41
|
self.dpop_iat_offset = dpop_iat_offset
|
|
42
|
+
self.client_id = client_id
|
|
43
|
+
self.client_secret = client_secret
|
|
@@ -94,3 +94,39 @@ class MissingAuthorizationError(BaseAuthError):
|
|
|
94
94
|
|
|
95
95
|
def get_error_code(self) -> str:
|
|
96
96
|
return "invalid_request"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class GetAccessTokenForConnectionError(BaseAuthError):
|
|
100
|
+
"""Error raised when getting a token for a connection fails."""
|
|
101
|
+
|
|
102
|
+
def get_status_code(self) -> int:
|
|
103
|
+
return 400
|
|
104
|
+
|
|
105
|
+
def get_error_code(self) -> str:
|
|
106
|
+
return "get_access_token_for_connection_error"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ApiError(BaseAuthError):
|
|
110
|
+
"""
|
|
111
|
+
Error raised when an API request to Auth0 fails.
|
|
112
|
+
Contains details about the original error from Auth0.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, code: str, message: str, status_code=500, cause=None):
|
|
116
|
+
super().__init__(message)
|
|
117
|
+
self.code = code
|
|
118
|
+
self.status_code = status_code
|
|
119
|
+
self.cause = cause
|
|
120
|
+
|
|
121
|
+
if cause:
|
|
122
|
+
self.error = getattr(cause, "error", None)
|
|
123
|
+
self.error_description = getattr(cause, "error_description", None)
|
|
124
|
+
else:
|
|
125
|
+
self.error = None
|
|
126
|
+
self.error_description = None
|
|
127
|
+
|
|
128
|
+
def get_status_code(self) -> int:
|
|
129
|
+
return self.status_code
|
|
130
|
+
|
|
131
|
+
def get_error_code(self) -> str:
|
|
132
|
+
return self.code
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|