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.
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/PKG-INFO +7 -5
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/README.md +3 -3
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/pyproject.toml +3 -2
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/auth_server/__init__.py +0 -1
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/auth_server/server_client.py +230 -123
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/auth_types/__init__.py +21 -31
- auth0_server_python-1.0.0b5/src/auth0_server_python/encryption/__init__.py +3 -0
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/encryption/encrypt.py +10 -11
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/error/__init__.py +27 -12
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/store/__init__.py +3 -3
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/store/abstract.py +29 -29
- auth0_server_python-1.0.0b5/src/auth0_server_python/tests/test_server_client.py +1254 -0
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/utils/__init__.py +3 -2
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/src/auth0_server_python/utils/helpers.py +39 -38
- auth0_server_python-1.0.0b4/src/auth0_server_python/encryption/__init__.py +0 -4
- {auth0_server_python-1.0.0b4 → auth0_server_python-1.0.0b5}/LICENSE +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: auth0-server-python
|
|
3
|
-
Version: 1.0.
|
|
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/
|
|
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](
|
|
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/
|
|
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/
|
|
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](
|
|
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/
|
|
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.
|
|
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=
|
|
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"]
|
|
@@ -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
|
|
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.
|
|
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[
|
|
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
|
-
) ->
|
|
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[
|
|
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[
|
|
391
|
-
) ->
|
|
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[
|
|
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[
|
|
464
|
-
) ->
|
|
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:
|
|
487
|
-
store_options: Optional[
|
|
488
|
-
) ->
|
|
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[
|
|
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[
|
|
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[
|
|
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:
|
|
640
|
-
store_options: Optional[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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:
|
|
843
|
-
) ->
|
|
839
|
+
options: dict[str, Any]
|
|
840
|
+
) -> dict[str, Any]:
|
|
844
841
|
"""
|
|
845
|
-
|
|
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
|
|
916
|
-
params.update(
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
"
|
|
989
|
-
|
|
1091
|
+
"invalid_response",
|
|
1092
|
+
"Failed to parse token response as JSON"
|
|
990
1093
|
)
|
|
991
1094
|
|
|
992
|
-
#
|
|
993
|
-
|
|
994
|
-
"
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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:
|
|
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:
|
|
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
|
|