auth0-server-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.
Files changed (16) hide show
  1. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/PKG-INFO +7 -5
  2. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/README.md +3 -3
  3. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/pyproject.toml +3 -2
  4. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/auth_server/__init__.py +0 -1
  5. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/auth_server/server_client.py +230 -123
  6. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/auth_types/__init__.py +21 -31
  7. auth0_server_python-1.0.0b5/src/auth0_server_python/encryption/__init__.py +3 -0
  8. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/encryption/encrypt.py +10 -11
  9. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/error/__init__.py +27 -12
  10. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/store/__init__.py +3 -3
  11. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/store/abstract.py +29 -29
  12. auth0_server_python-1.0.0b5/src/auth0_server_python/tests/test_server_client.py +1254 -0
  13. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/utils/__init__.py +3 -2
  14. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/utils/helpers.py +39 -38
  15. auth0_server_python-1.0.0b4/src/auth0_server_python/encryption/__init__.py +0 -4
  16. {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/LICENSE +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: auth0-server-python
3
- Version: 1.0.0b4
3
+ Version: 1.0.0b5
4
4
  Summary: Auth0 server-side Python SDK
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: Auth0
7
8
  Author-email: support@okta.com
8
9
  Requires-Python: >=3.9
@@ -13,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
13
14
  Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
16
18
  Requires-Dist: authlib (>=1.2,<2.0)
17
19
  Requires-Dist: cryptography (>=43.0.1)
18
20
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
@@ -29,7 +31,7 @@ The Auth0 Server Python SDK is a library for implementing user authentication in
29
31
 
30
32
  ## Documentation
31
33
 
32
- - [Examples](https://github.com/auth0/auth0-server-python/blob/main/packages/auth0_server_python/EXAMPLES.md) - examples for your different use cases.
34
+ - [Examples](https://github.com/auth0/auth0-server-python/blob/main/examples) - examples for your different use cases.
33
35
  - [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
34
36
 
35
37
  ## Getting Started
@@ -132,7 +134,7 @@ We appreciate feedback and contribution to this repo! Before you get started, pl
132
134
 
133
135
  - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
134
136
  - [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
135
- - [This repo's contribution guide](./../../CONTRIBUTING.md)
137
+ - [This repo's contribution guide](./CONTRIBUTING.md)
136
138
 
137
139
  ### Raise an issue
138
140
 
@@ -155,5 +157,5 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
155
157
  Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a>
156
158
  </p>
157
159
  <p align="center">
158
- This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-server-python/blob/main/packages/auth0_server_python/LICENSE"> LICENSE</a> file for more info.
160
+ This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-server-python/blob/main/LICENSE"> LICENSE</a> file for more info.
159
161
  </p>
@@ -6,7 +6,7 @@ The Auth0 Server Python SDK is a library for implementing user authentication in
6
6
 
7
7
  ## Documentation
8
8
 
9
- - [Examples](https://github.com/auth0/auth0-server-python/blob/main/packages/auth0_server_python/EXAMPLES.md) - examples for your different use cases.
9
+ - [Examples](https://github.com/auth0/auth0-server-python/blob/main/examples) - examples for your different use cases.
10
10
  - [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
11
11
 
12
12
  ## Getting Started
@@ -109,7 +109,7 @@ We appreciate feedback and contribution to this repo! Before you get started, pl
109
109
 
110
110
  - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
111
111
  - [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
112
- - [This repo's contribution guide](./../../CONTRIBUTING.md)
112
+ - [This repo's contribution guide](./CONTRIBUTING.md)
113
113
 
114
114
  ### Raise an issue
115
115
 
@@ -132,5 +132,5 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
132
132
  Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a>
133
133
  </p>
134
134
  <p align="center">
135
- This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-server-python/blob/main/packages/auth0_server_python/LICENSE"> LICENSE</a> file for more info.
135
+ This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-server-python/blob/main/LICENSE"> LICENSE</a> file for more info.
136
136
  </p>
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-server-python"
3
- version = "1.0.0.b4"
3
+ version = "1.0.0.b5"
4
4
  description = "Auth0 server-side Python SDK"
5
5
  readme = "README.md"
6
6
  authors = ["Auth0 <support@okta.com>"]
@@ -25,9 +25,10 @@ pytest-cov = "^4.0"
25
25
  pytest-asyncio = "^0.20.3"
26
26
  pytest-mock = "^3.14.0"
27
27
  twine = "^6.1.0"
28
+ ruff = "^0.1.0"
28
29
 
29
30
  [tool.pytest.ini_options]
30
- addopts = "--cov=auth_server --cov-report=term-missing:skip-covered --cov-report=xml"
31
+ addopts = "--cov=auth0_server_python --cov-report=term-missing:skip-covered --cov-report=xml"
31
32
 
32
33
  [build-system]
33
34
  requires = ["poetry-core>=1.4.0"]
@@ -1,4 +1,3 @@
1
1
  from .server_client import ServerClient
2
2
 
3
-
4
3
  __all__ = ["ServerClient"]
@@ -3,42 +3,39 @@ Main client for auth0-server-python SDK.
3
3
  Handles authentication flows, token management, and user sessions.
4
4
  """
5
5
 
6
- import time
7
- from typing import Dict, Any, Optional, List, Union, TypeVar, Generic, Callable
8
- from urllib.parse import urlparse, parse_qs
9
- import json
10
6
  import asyncio
11
- import jwt
7
+ import json
8
+ import time
9
+ from typing import Any, Generic, Optional, TypeVar
10
+ from urllib.parse import parse_qs, urlparse
12
11
 
13
- from authlib.integrations.httpx_client import AsyncOAuth2Client
14
- from authlib.integrations.base_client.errors import OAuthError
15
12
  import httpx
16
-
17
- from pydantic import BaseModel, ValidationError
18
-
19
- from auth0_server_python.error import (
20
- MissingTransactionError,
21
- ApiError,
22
- MissingRequiredArgumentError,
23
- BackchannelLogoutError,
24
- AccessTokenError,
25
- AccessTokenForConnectionError,
26
- StartLinkUserError,
27
- AccessTokenErrorCode,
28
- AccessTokenForConnectionErrorCode
29
-
30
- )
13
+ import jwt
31
14
  from auth0_server_python.auth_types import (
15
+ LogoutOptions,
16
+ LogoutTokenClaims,
17
+ StartInteractiveLoginOptions,
32
18
  StateData,
19
+ TokenSet,
33
20
  TransactionData,
34
21
  UserClaims,
35
- TokenSet,
36
- LogoutTokenClaims,
37
- StartInteractiveLoginOptions,
38
- LogoutOptions
39
22
  )
40
- from auth0_server_python.utils import PKCE, State, URL
41
-
23
+ from auth0_server_python.error import (
24
+ AccessTokenError,
25
+ AccessTokenErrorCode,
26
+ AccessTokenForConnectionError,
27
+ AccessTokenForConnectionErrorCode,
28
+ ApiError,
29
+ BackchannelLogoutError,
30
+ MissingRequiredArgumentError,
31
+ MissingTransactionError,
32
+ PollingApiError,
33
+ StartLinkUserError,
34
+ )
35
+ from auth0_server_python.utils import PKCE, URL, State
36
+ from authlib.integrations.base_client.errors import OAuthError
37
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
38
+ from pydantic import ValidationError
42
39
 
43
40
  # Generic type for store options
44
41
  TStoreOptions = TypeVar('TStoreOptions')
@@ -63,7 +60,7 @@ class ServerClient(Generic[TStoreOptions]):
63
60
  state_store=None,
64
61
  transaction_identifier: str = "_a0_tx",
65
62
  state_identifier: str = "_a0_session",
66
- authorization_params: Optional[Dict[str, Any]] = None,
63
+ authorization_params: Optional[dict[str, Any]] = None,
67
64
  pushed_authorization_requests: bool = False
68
65
  ):
69
66
  """
@@ -224,7 +221,7 @@ class ServerClient(Generic[TStoreOptions]):
224
221
  self,
225
222
  url: str,
226
223
  store_options: dict = None
227
- ) -> Dict[str, Any]:
224
+ ) -> dict[str, Any]:
228
225
  """
229
226
  Completes the login process after user is redirected back.
230
227
 
@@ -337,7 +334,7 @@ class ServerClient(Generic[TStoreOptions]):
337
334
  async def start_link_user(
338
335
  self,
339
336
  options,
340
- store_options: Optional[Dict[str, Any]] = None
337
+ store_options: Optional[dict[str, Any]] = None
341
338
  ):
342
339
  """
343
340
  Starts the user linking process, and returns a URL to redirect the user-agent to.
@@ -387,8 +384,8 @@ class ServerClient(Generic[TStoreOptions]):
387
384
  async def complete_link_user(
388
385
  self,
389
386
  url: str,
390
- store_options: Optional[Dict[str, Any]] = None
391
- ) -> Dict[str, Any]:
387
+ store_options: Optional[dict[str, Any]] = None
388
+ ) -> dict[str, Any]:
392
389
  """
393
390
  Completes the user linking process.
394
391
 
@@ -411,7 +408,7 @@ class ServerClient(Generic[TStoreOptions]):
411
408
  async def start_unlink_user(
412
409
  self,
413
410
  options,
414
- store_options: Optional[Dict[str, Any]] = None
411
+ store_options: Optional[dict[str, Any]] = None
415
412
  ):
416
413
  """
417
414
  Starts the user unlinking process, and returns a URL to redirect the user-agent to.
@@ -460,8 +457,8 @@ class ServerClient(Generic[TStoreOptions]):
460
457
  async def complete_unlink_user(
461
458
  self,
462
459
  url: str,
463
- store_options: Optional[Dict[str, Any]] = None
464
- ) -> Dict[str, Any]:
460
+ store_options: Optional[dict[str, Any]] = None
461
+ ) -> dict[str, Any]:
465
462
  """
466
463
  Completes the user unlinking process.
467
464
 
@@ -483,9 +480,9 @@ class ServerClient(Generic[TStoreOptions]):
483
480
 
484
481
  async def login_backchannel(
485
482
  self,
486
- options: Dict[str, Any],
487
- store_options: Optional[Dict[str, Any]] = None
488
- ) -> Dict[str, Any]:
483
+ options: dict[str, Any],
484
+ store_options: Optional[dict[str, Any]] = None
485
+ ) -> dict[str, Any]:
489
486
  """
490
487
  Logs in using Client-Initiated Backchannel Authentication.
491
488
 
@@ -527,7 +524,7 @@ class ServerClient(Generic[TStoreOptions]):
527
524
  }
528
525
  return result
529
526
 
530
- async def get_user(self, store_options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
527
+ async def get_user(self, store_options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
531
528
  """
532
529
  Retrieves the user from the store, or None if no user found.
533
530
 
@@ -545,7 +542,7 @@ class ServerClient(Generic[TStoreOptions]):
545
542
  return state_data.get("user")
546
543
  return None
547
544
 
548
- async def get_session(self, store_options: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
545
+ async def get_session(self, store_options: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]:
549
546
  """
550
547
  Retrieve the user session from the store, or None if no session found.
551
548
 
@@ -565,7 +562,7 @@ class ServerClient(Generic[TStoreOptions]):
565
562
  return session_data
566
563
  return None
567
564
 
568
- async def get_access_token(self, store_options: Optional[Dict[str, Any]] = None) -> str:
565
+ async def get_access_token(self, store_options: Optional[dict[str, Any]] = None) -> str:
569
566
  """
570
567
  Retrieves the access token from the store, or calls Auth0 when the access token
571
568
  is expired and a refresh token is available in the store.
@@ -636,8 +633,8 @@ class ServerClient(Generic[TStoreOptions]):
636
633
 
637
634
  async def get_access_token_for_connection(
638
635
  self,
639
- options: Dict[str, Any],
640
- store_options: Optional[Dict[str, Any]] = None
636
+ options: dict[str, Any],
637
+ store_options: Optional[dict[str, Any]] = None
641
638
  ) -> str:
642
639
  """
643
640
  Retrieves an access token for a connection.
@@ -702,7 +699,7 @@ class ServerClient(Generic[TStoreOptions]):
702
699
  async def logout(
703
700
  self,
704
701
  options: Optional[LogoutOptions] = None,
705
- store_options: Optional[Dict[str, Any]] = None
702
+ store_options: Optional[dict[str, Any]] = None
706
703
  ) -> str:
707
704
  options = options or LogoutOptions()
708
705
 
@@ -718,7 +715,7 @@ class ServerClient(Generic[TStoreOptions]):
718
715
  async def handle_backchannel_logout(
719
716
  self,
720
717
  logout_token: str,
721
- store_options: Optional[Dict[str, Any]] = None
718
+ store_options: Optional[dict[str, Any]] = None
722
719
  ) -> None:
723
720
  """
724
721
  Handles backchannel logout requests.
@@ -762,7 +759,7 @@ class ServerClient(Generic[TStoreOptions]):
762
759
  code_verifier: str,
763
760
  state: str,
764
761
  connection_scope: Optional[str] = None,
765
- authorization_params: Optional[Dict[str, Any]] = None
762
+ authorization_params: Optional[dict[str, Any]] = None
766
763
  ) -> str:
767
764
  """Build a URL for linking user accounts"""
768
765
  # Generate code challenge from verifier
@@ -805,7 +802,7 @@ class ServerClient(Generic[TStoreOptions]):
805
802
  id_token: str,
806
803
  code_verifier: str,
807
804
  state: str,
808
- authorization_params: Optional[Dict[str, Any]] = None
805
+ authorization_params: Optional[dict[str, Any]] = None
809
806
  ) -> str:
810
807
  """Build a URL for unlinking user accounts"""
811
808
  # Generate code challenge from verifier
@@ -839,17 +836,24 @@ class ServerClient(Generic[TStoreOptions]):
839
836
 
840
837
  async def backchannel_authentication(
841
838
  self,
842
- options: Dict[str, Any]
843
- ) -> Dict[str, Any]:
839
+ options: dict[str, Any]
840
+ ) -> dict[str, Any]:
844
841
  """
845
- Initiates backchannel authentication with Auth0.
842
+ Performs backchannel authentication with Auth0.
846
843
 
847
844
  This method starts a Client-Initiated Backchannel Authentication (CIBA) flow,
848
845
  which allows an application to request authentication from a user via a separate
849
846
  device or channel.
850
847
 
848
+ Then polls the token endpoint until the user has authenticated or the request times out.
849
+
851
850
  Args:
852
- options: Configuration options for backchannel authentication
851
+ options (dict): Configuration options for backchannel authentication
852
+ - login_hint (dict): Must contain a 'sub' field (e.g., {'sub': 'user_id'}).
853
+ - binding_message (str, optional): Message to display to the user.
854
+ - authorization_params (dict, optional): Additional authorization parameters.
855
+ - requested_expiry (int, optional): Requested expiry time in seconds, default is 30 secs.
856
+ - authorization_details (str, optional): JSON string for RAR.
853
857
 
854
858
  Returns:
855
859
  Token response data from the backchannel authentication
@@ -857,6 +861,97 @@ class ServerClient(Generic[TStoreOptions]):
857
861
  Raises:
858
862
  ApiError: If the backchannel authentication fails
859
863
  """
864
+ backchannel_data = await self.initiate_backchannel_authentication(options)
865
+ auth_req_id = backchannel_data.get("auth_req_id")
866
+ expires_in = backchannel_data.get(
867
+ "expires_in", 120) # Default to 2 minutes
868
+ interval = backchannel_data.get(
869
+ "interval", 5) # Default to 5 seconds
870
+
871
+ # Calculate when to stop polling
872
+ end_time = time.time() + expires_in
873
+
874
+ # Poll until we get a response or timeout
875
+ while time.time() < end_time:
876
+ # Make token request
877
+ try:
878
+ token_response = await self.backchannel_authentication_grant(auth_req_id)
879
+ return token_response
880
+
881
+ except Exception as e:
882
+ if isinstance(e, PollingApiError):
883
+ if e.code == "authorization_pending":
884
+ # Wait for the specified interval before polling again
885
+ await asyncio.sleep(interval)
886
+ continue
887
+ if e.code == "slow_down":
888
+ # Wait for the specified interval before polling again
889
+ await asyncio.sleep(e.interval or interval)
890
+ continue
891
+ raise ApiError(
892
+ "backchannel_error",
893
+ f"Backchannel authentication failed: {str(e) or 'Unknown error'}",
894
+ e
895
+ )
896
+
897
+ # If we get here, we've timed out
898
+ raise ApiError(
899
+ "timeout", "Backchannel authentication timed out")
900
+
901
+ async def initiate_backchannel_authentication(
902
+ self,
903
+ options: dict[str, Any]
904
+ ) -> dict[str, Any]:
905
+ """
906
+ Start backchannel authentication with Auth0.
907
+
908
+ This method starts a Client-Initiated Backchannel Authentication (CIBA) flow,
909
+ which allows an application to request authentication from a user via a separate
910
+ device or channel.
911
+
912
+ Args:
913
+ options (dict): Configuration options for backchannel authentication
914
+ - login_hint (dict): Must contain a 'sub' field (e.g., {'sub': 'user_id'}).
915
+ - binding_message (str, optional): Message to display to the user.
916
+ - authorization_params (dict, optional): Additional authorization parameters.
917
+ - requested_expiry (int, optional): Requested expiry time in seconds, default is 30 secs.
918
+ - authorization_details (str, optional): JSON string for RAR.
919
+
920
+ Returns:
921
+ dict: Response data from the bc-authorize backchannel authentication
922
+ - auth_req_id (str): The authentication request ID.
923
+ - expires_in (int): Time in seconds until the request expires.
924
+ - interval (int, optional): Polling interval in seconds.
925
+
926
+ Raises:
927
+ ApiError: If the backchannel authentication fails
928
+
929
+ See:
930
+ https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow
931
+ """
932
+
933
+ sub = options.get('login_hint', {}).get("sub")
934
+ if not sub:
935
+ raise MissingRequiredArgumentError(
936
+ "login_hint.sub"
937
+ )
938
+
939
+ authorization_params = options.get('authorization_params')
940
+ if authorization_params is not None and not isinstance(authorization_params, dict):
941
+ raise ApiError(
942
+ "invalid_argument",
943
+ "authorization_params must be a dict"
944
+ )
945
+
946
+ if authorization_params:
947
+ requested_expiry = authorization_params.get("requested_expiry")
948
+ if requested_expiry is not None:
949
+ if not isinstance(requested_expiry, int) or requested_expiry <= 0:
950
+ raise ApiError(
951
+ "invalid_argument",
952
+ "authorization_params.requested_expiry must be a positive integer"
953
+ )
954
+
860
955
  try:
861
956
  # Fetch OpenID Connect metadata if not already fetched
862
957
  if not hasattr(self, '_oauth_metadata'):
@@ -875,21 +970,6 @@ class ServerClient(Generic[TStoreOptions]):
875
970
  "Backchannel authentication is not supported by the authorization server"
876
971
  )
877
972
 
878
- # Get token endpoint
879
- token_endpoint = self._oauth_metadata.get("token_endpoint")
880
- if not token_endpoint:
881
- raise ApiError(
882
- "configuration_error",
883
- "Token endpoint is missing in OIDC metadata"
884
- )
885
-
886
- sub = sub = options.get('login_hint', {}).get("sub")
887
- if not sub:
888
- raise ApiError(
889
- "invalid_parameter",
890
- "login_hint must contain a 'sub' field"
891
- )
892
-
893
973
  # Prepare login hint in the required format
894
974
  login_hint = json.dumps({
895
975
  "format": "iss_sub",
@@ -912,8 +992,8 @@ class ServerClient(Generic[TStoreOptions]):
912
992
  if self._default_authorization_params:
913
993
  params.update(self._default_authorization_params)
914
994
 
915
- if options.get('authorization_params'):
916
- params.update(options.get('authorization_params'))
995
+ if authorization_params:
996
+ params.update(authorization_params)
917
997
 
918
998
  # Make the backchannel authentication request
919
999
  async with httpx.AsyncClient() as client:
@@ -933,10 +1013,6 @@ class ServerClient(Generic[TStoreOptions]):
933
1013
 
934
1014
  backchannel_data = backchannel_response.json()
935
1015
  auth_req_id = backchannel_data.get("auth_req_id")
936
- expires_in = backchannel_data.get(
937
- "expires_in", 120) # Default to 2 minutes
938
- interval = backchannel_data.get(
939
- "interval", 5) # Default to 5 seconds
940
1016
 
941
1017
  if not auth_req_id:
942
1018
  raise ApiError(
@@ -944,64 +1020,95 @@ class ServerClient(Generic[TStoreOptions]):
944
1020
  "Missing auth_req_id in backchannel authentication response"
945
1021
  )
946
1022
 
947
- # Poll for token using the auth_req_id
948
- token_params = {
949
- "grant_type": "urn:openid:params:grant-type:ciba",
950
- "auth_req_id": auth_req_id,
951
- "client_id": self._client_id,
952
- "client_secret": self._client_secret
953
- }
1023
+ return backchannel_data
1024
+
1025
+ except Exception as e:
1026
+ if isinstance(e, ApiError):
1027
+ raise
1028
+ raise ApiError(
1029
+ "backchannel_error",
1030
+ f"Backchannel authentication failed: {str(e) or 'Unknown error'}",
1031
+ e
1032
+ )
1033
+
1034
+ async def backchannel_authentication_grant(self, auth_req_id: str) -> dict[str, Any]:
1035
+ """
1036
+ Retrieves a token by exchanging an auth_req_id.
1037
+
1038
+ Args:
1039
+ auth_req_id (str): The authentication request ID obtained from bc-authorize
1040
+
1041
+ Raises:
1042
+ AccessTokenError: If there was an issue requesting the access token.
1043
+
1044
+ Returns:
1045
+ A dictionary containing the token response from Auth0.
1046
+ """
1047
+ if not auth_req_id:
1048
+ raise MissingRequiredArgumentError("auth_req_id")
1049
+
1050
+ try:
1051
+ # Ensure we have the OIDC metadata
1052
+ if not hasattr(self._oauth, "metadata") or not self._oauth.metadata:
1053
+ self._oauth.metadata = await self._fetch_oidc_metadata(self._domain)
1054
+
1055
+ token_endpoint = self._oauth.metadata.get("token_endpoint")
1056
+ if not token_endpoint:
1057
+ raise ApiError("configuration_error",
1058
+ "Token endpoint missing in OIDC metadata")
1059
+
1060
+ # Prepare the token request parameters
1061
+ token_params = {
1062
+ "grant_type": "urn:openid:params:grant-type:ciba",
1063
+ "auth_req_id": auth_req_id,
1064
+ "client_id": self._client_id,
1065
+ "client_secret": self._client_secret
1066
+ }
1067
+
1068
+ # Exchange the auth_req_id for an access token
1069
+ async with httpx.AsyncClient() as client:
1070
+ response = await client.post(
1071
+ token_endpoint,
1072
+ data=token_params,
1073
+ auth=(self._client_id, self._client_secret)
1074
+ )
954
1075
 
955
- # Calculate when to stop polling
956
- end_time = time.time() + expires_in
957
-
958
- # Poll until we get a response or timeout
959
- while time.time() < end_time:
960
- # Make token request
961
- token_response = await client.post(token_endpoint, data=token_params)
962
-
963
- # Check for success (200 OK)
964
- if token_response.status_code == 200:
965
- # Success! Parse and return the token response
966
- return token_response.json()
967
-
968
- # Check for specific error that indicates we should continue polling
969
- if token_response.status_code == 400:
970
- error_data = token_response.json()
971
- error = error_data.get("error")
972
-
973
- # authorization_pending means we should keep polling
974
- if error == "authorization_pending":
975
- # Wait for the specified interval before polling again
976
- await asyncio.sleep(interval)
977
- continue
978
-
979
- # Other errors should be raised
980
- raise ApiError(
981
- error,
982
- error_data.get("error_description",
983
- "Token request failed")
984
- )
985
-
986
- # Any other status code is an error
1076
+ if response.status_code != 200:
1077
+ error_data = response.json()
1078
+ retry_after = response.headers.get("Retry-After")
1079
+ interval = int(retry_after) if retry_after is not None else None
1080
+ raise PollingApiError(
1081
+ error_data.get("error", "auth_req_id_error"),
1082
+ error_data.get("error_description",
1083
+ "Failed to exchange auth_req_id"),
1084
+ interval
1085
+ )
1086
+
1087
+ try:
1088
+ token_response = response.json()
1089
+ except json.JSONDecodeError:
987
1090
  raise ApiError(
988
- "token_error",
989
- f"Unexpected status code: {token_response.status_code}"
1091
+ "invalid_response",
1092
+ "Failed to parse token response as JSON"
990
1093
  )
991
1094
 
992
- # If we get here, we've timed out
993
- raise ApiError(
994
- "timeout", "Backchannel authentication timed out")
1095
+ # Add required fields if they are missing
1096
+ if "expires_in" in token_response and "expires_at" not in token_response:
1097
+ token_response["expires_at"] = int(
1098
+ time.time()) + token_response["expires_in"]
1099
+
1100
+ return token_response
995
1101
 
996
1102
  except Exception as e:
997
- print("Caught exception:", type(e), e.args, repr(e))
998
- raise ApiError(
999
- "backchannel_error",
1000
- f"Backchannel authentication failed: {str(e) or 'Unknown error'}",
1103
+ if isinstance(e, (ApiError, PollingApiError)):
1104
+ raise
1105
+ raise AccessTokenError(
1106
+ AccessTokenErrorCode.AUTH_REQ_ID_ERROR,
1107
+ "There was an error while trying to exchange the auth_req_id for an access token.",
1001
1108
  e
1002
1109
  )
1003
1110
 
1004
- async def get_token_by_refresh_token(self, options: Dict[str, Any]) -> Dict[str, Any]:
1111
+ async def get_token_by_refresh_token(self, options: dict[str, Any]) -> dict[str, Any]:
1005
1112
  """
1006
1113
  Retrieves a token by exchanging a refresh token.
1007
1114
 
@@ -1074,7 +1181,7 @@ class ServerClient(Generic[TStoreOptions]):
1074
1181
  e
1075
1182
  )
1076
1183
 
1077
- async def get_token_for_connection(self, options: Dict[str, Any]) -> Dict[str, Any]:
1184
+ async def get_token_for_connection(self, options: dict[str, Any]) -> dict[str, Any]:
1078
1185
  """
1079
1186
  Retrieves a token for a connection.
1080
1187