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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: auth0-api-python
3
- Version: 1.0.0b4
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
- ### 4. DPoP Authentication
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
- ### 4. DPoP Authentication
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-api-python"
3
- version = "1.0.0.b4"
3
+ version = "1.0.0.b5"
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"
@@ -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
- self,
23
- domain: str,
24
- audience: str,
25
- custom_fetch: Optional[Callable[..., object]] = None,
26
- dpop_enabled: bool = True,
27
- dpop_required: bool = False,
28
- dpop_iat_leeway: int = 30,
29
- dpop_iat_offset: int = 300,
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