auth0-server-python 1.0.0b14__tar.gz → 1.0.0b16__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.0b14 → auth0_server_python-1.0.0b16}/PKG-INFO +59 -3
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/README.md +58 -2
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/pyproject.toml +1 -1
- auth0_server_python-1.0.0b16/src/auth0_server_python/auth_schemes/client_assertion.py +63 -0
- auth0_server_python-1.0.0b16/src/auth0_server_python/auth_server/__init__.py +6 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_server/mfa_client.py +57 -7
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_server/my_account_client.py +13 -1
- auth0_server_python-1.0.0b16/src/auth0_server_python/auth_server/passwordless_client.py +478 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_server/server_client.py +421 -53
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_types/__init__.py +165 -4
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/error/__init__.py +83 -21
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_mfa_client.py +161 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_my_account_client.py +22 -0
- auth0_server_python-1.0.0b16/src/auth0_server_python/tests/test_passwordless_client.py +1300 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_server_client.py +1345 -68
- auth0_server_python-1.0.0b14/src/auth0_server_python/auth_server/__init__.py +0 -5
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/LICENSE +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_schemes/dpop_auth.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/encryption/__init__.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/encryption/encrypt.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/store/__init__.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/store/abstract.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/telemetry.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_dpop_auth.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_telemetry.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/utils/__init__.py +0 -0
- {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/utils/helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auth0-server-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b16
|
|
4
4
|
Summary: Auth0 server-side Python SDK
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -48,7 +48,7 @@ pip install auth0-server-python
|
|
|
48
48
|
If you’re using Poetry:
|
|
49
49
|
|
|
50
50
|
```shell
|
|
51
|
-
poetry
|
|
51
|
+
poetry add auth0-server-python
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
### 2. Create the Auth0 SDK client
|
|
@@ -65,7 +65,7 @@ auth0 = ServerClient(
|
|
|
65
65
|
client_secret='<AUTH0_CLIENT_SECRET>',
|
|
66
66
|
secret='<AUTH0_SECRET>',
|
|
67
67
|
authorization_params= {
|
|
68
|
-
redirect_uri: '<AUTH0_REDIRECT_URI>',
|
|
68
|
+
'redirect_uri': '<AUTH0_REDIRECT_URI>',
|
|
69
69
|
}
|
|
70
70
|
)
|
|
71
71
|
```
|
|
@@ -80,6 +80,58 @@ The `AUTH0_SECRET` is the key used to encrypt the session and transaction cookie
|
|
|
80
80
|
openssl rand -hex 64
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
#### Authenticating with Private Key JWT
|
|
84
|
+
|
|
85
|
+
The SDK authenticates to Auth0 with either a client secret or a Private Key JWT (`private_key_jwt`). To use Private Key JWT, pass your private signing key as `client_assertion_signing_key` instead of `client_secret`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from auth0_server_python.auth_server.server_client import ServerClient
|
|
89
|
+
|
|
90
|
+
with open('private_key.pem') as f:
|
|
91
|
+
private_key = f.read()
|
|
92
|
+
|
|
93
|
+
auth0 = ServerClient(
|
|
94
|
+
domain='<AUTH0_DOMAIN>',
|
|
95
|
+
client_id='<AUTH0_CLIENT_ID>',
|
|
96
|
+
client_assertion_signing_key=private_key,
|
|
97
|
+
secret='<AUTH0_SECRET>',
|
|
98
|
+
authorization_params={
|
|
99
|
+
'redirect_uri': '<AUTH0_REDIRECT_URI>',
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The key must be a PKCS8 PEM private key. Register its public key on your Auth0 application under **Settings → Credentials**, and set the application's authentication method to Private Key JWT. The signing algorithm defaults to `RS256` and can be overridden with `client_assertion_signing_alg`. Auth0 accepts `RS256`, `RS384`, and `PS256`, all of which use an RSA key. The algorithm must match the key type and the algorithm chosen when the public key credential was created.
|
|
105
|
+
|
|
106
|
+
> [!NOTE]
|
|
107
|
+
> The passkey challenge endpoints (`register` and `challenge`) accept only a client secret, so a client configured with just a signing key cannot use them.
|
|
108
|
+
|
|
109
|
+
> [!IMPORTANT]
|
|
110
|
+
> Private keys must not be committed to source control. Load them from a secure secret store or an environment-provided file.
|
|
111
|
+
|
|
112
|
+
#### Authenticating with Mutual TLS (mTLS)
|
|
113
|
+
|
|
114
|
+
The SDK supports mTLS client authentication (RFC 8705): the client presents a TLS certificate during the handshake instead of a client secret. Pass `use_mtls=True` and a caller-built `ssl.SSLContext` that already has the certificate loaded:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import ssl
|
|
118
|
+
|
|
119
|
+
ssl_context = ssl.create_default_context()
|
|
120
|
+
ssl_context.load_cert_chain("client.crt", "client.key")
|
|
121
|
+
|
|
122
|
+
auth0 = ServerClient(
|
|
123
|
+
domain="login.example.com", # self_managed_certs custom domain
|
|
124
|
+
client_id="<AUTH0_CLIENT_ID>",
|
|
125
|
+
use_mtls=True,
|
|
126
|
+
ssl_context=ssl_context,
|
|
127
|
+
secret="<AUTH0_SECRET>",
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`use_mtls=True` requires an Enterprise tenant with the Highly Regulated Identity add-on, a `self_managed_certs` custom domain, and mTLS endpoint aliases enabled. It cannot be combined with `client_secret`, `client_assertion_signing_key`, or a per-call `dpop_key`. Each raises `ConfigurationError`. See the [Auth0 mTLS configuration docs](https://auth0.com/docs/get-started/applications/configure-mtls) for tenant-side setup steps.
|
|
132
|
+
|
|
133
|
+
See [examples/MutualTLS.md](examples/MutualTLS.md) for the full setup guide, certificate generation, and token sender-constraining details.
|
|
134
|
+
|
|
83
135
|
### 3. Add login to your Application (interactive)
|
|
84
136
|
|
|
85
137
|
Before using redirect-based login, ensure the `redirect_uri` is configured when initializing the SDK:
|
|
@@ -225,6 +277,10 @@ Let a logged-in user manage their own enrolled authentication methods — enroll
|
|
|
225
277
|
|
|
226
278
|
Bind tokens to a key your server holds ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) so a stolen token alone cannot be replayed. DPoP is supported for Passkey sign-in (`signin_with_passkey`) and the authentication-methods/factors methods on `MyAccountClient`. For key generation and usage, see [examples/Passkeys.md](examples/Passkeys.md#3-dpop-bound-passkey-tokens-optional) and [examples/MyAccountAuthenticationMethods.md](examples/MyAccountAuthenticationMethods.md#dpop).
|
|
227
279
|
|
|
280
|
+
### 10. Passwordless Authentication
|
|
281
|
+
|
|
282
|
+
Sign users in with a one-time code sent by email or SMS, or with a magic link sent by email, via [Auth0 embedded passwordless login](https://auth0.com/docs/authenticate/passwordless/implement-login/embedded-login/relevant-api-endpoints). OTP verification and the magic-link callback each establish a server-side session like every other login path. For prerequisites, both flows, custom scopes/audiences, step-up MFA, and error handling, see [examples/Passwordless.md](examples/Passwordless.md).
|
|
283
|
+
|
|
228
284
|
## Feedback
|
|
229
285
|
|
|
230
286
|
### Contributing
|
|
@@ -23,7 +23,7 @@ pip install auth0-server-python
|
|
|
23
23
|
If you’re using Poetry:
|
|
24
24
|
|
|
25
25
|
```shell
|
|
26
|
-
poetry
|
|
26
|
+
poetry add auth0-server-python
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### 2. Create the Auth0 SDK client
|
|
@@ -40,7 +40,7 @@ auth0 = ServerClient(
|
|
|
40
40
|
client_secret='<AUTH0_CLIENT_SECRET>',
|
|
41
41
|
secret='<AUTH0_SECRET>',
|
|
42
42
|
authorization_params= {
|
|
43
|
-
redirect_uri: '<AUTH0_REDIRECT_URI>',
|
|
43
|
+
'redirect_uri': '<AUTH0_REDIRECT_URI>',
|
|
44
44
|
}
|
|
45
45
|
)
|
|
46
46
|
```
|
|
@@ -55,6 +55,58 @@ The `AUTH0_SECRET` is the key used to encrypt the session and transaction cookie
|
|
|
55
55
|
openssl rand -hex 64
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
#### Authenticating with Private Key JWT
|
|
59
|
+
|
|
60
|
+
The SDK authenticates to Auth0 with either a client secret or a Private Key JWT (`private_key_jwt`). To use Private Key JWT, pass your private signing key as `client_assertion_signing_key` instead of `client_secret`:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from auth0_server_python.auth_server.server_client import ServerClient
|
|
64
|
+
|
|
65
|
+
with open('private_key.pem') as f:
|
|
66
|
+
private_key = f.read()
|
|
67
|
+
|
|
68
|
+
auth0 = ServerClient(
|
|
69
|
+
domain='<AUTH0_DOMAIN>',
|
|
70
|
+
client_id='<AUTH0_CLIENT_ID>',
|
|
71
|
+
client_assertion_signing_key=private_key,
|
|
72
|
+
secret='<AUTH0_SECRET>',
|
|
73
|
+
authorization_params={
|
|
74
|
+
'redirect_uri': '<AUTH0_REDIRECT_URI>',
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The key must be a PKCS8 PEM private key. Register its public key on your Auth0 application under **Settings → Credentials**, and set the application's authentication method to Private Key JWT. The signing algorithm defaults to `RS256` and can be overridden with `client_assertion_signing_alg`. Auth0 accepts `RS256`, `RS384`, and `PS256`, all of which use an RSA key. The algorithm must match the key type and the algorithm chosen when the public key credential was created.
|
|
80
|
+
|
|
81
|
+
> [!NOTE]
|
|
82
|
+
> The passkey challenge endpoints (`register` and `challenge`) accept only a client secret, so a client configured with just a signing key cannot use them.
|
|
83
|
+
|
|
84
|
+
> [!IMPORTANT]
|
|
85
|
+
> Private keys must not be committed to source control. Load them from a secure secret store or an environment-provided file.
|
|
86
|
+
|
|
87
|
+
#### Authenticating with Mutual TLS (mTLS)
|
|
88
|
+
|
|
89
|
+
The SDK supports mTLS client authentication (RFC 8705): the client presents a TLS certificate during the handshake instead of a client secret. Pass `use_mtls=True` and a caller-built `ssl.SSLContext` that already has the certificate loaded:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import ssl
|
|
93
|
+
|
|
94
|
+
ssl_context = ssl.create_default_context()
|
|
95
|
+
ssl_context.load_cert_chain("client.crt", "client.key")
|
|
96
|
+
|
|
97
|
+
auth0 = ServerClient(
|
|
98
|
+
domain="login.example.com", # self_managed_certs custom domain
|
|
99
|
+
client_id="<AUTH0_CLIENT_ID>",
|
|
100
|
+
use_mtls=True,
|
|
101
|
+
ssl_context=ssl_context,
|
|
102
|
+
secret="<AUTH0_SECRET>",
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`use_mtls=True` requires an Enterprise tenant with the Highly Regulated Identity add-on, a `self_managed_certs` custom domain, and mTLS endpoint aliases enabled. It cannot be combined with `client_secret`, `client_assertion_signing_key`, or a per-call `dpop_key`. Each raises `ConfigurationError`. See the [Auth0 mTLS configuration docs](https://auth0.com/docs/get-started/applications/configure-mtls) for tenant-side setup steps.
|
|
107
|
+
|
|
108
|
+
See [examples/MutualTLS.md](examples/MutualTLS.md) for the full setup guide, certificate generation, and token sender-constraining details.
|
|
109
|
+
|
|
58
110
|
### 3. Add login to your Application (interactive)
|
|
59
111
|
|
|
60
112
|
Before using redirect-based login, ensure the `redirect_uri` is configured when initializing the SDK:
|
|
@@ -200,6 +252,10 @@ Let a logged-in user manage their own enrolled authentication methods — enroll
|
|
|
200
252
|
|
|
201
253
|
Bind tokens to a key your server holds ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)) so a stolen token alone cannot be replayed. DPoP is supported for Passkey sign-in (`signin_with_passkey`) and the authentication-methods/factors methods on `MyAccountClient`. For key generation and usage, see [examples/Passkeys.md](examples/Passkeys.md#3-dpop-bound-passkey-tokens-optional) and [examples/MyAccountAuthenticationMethods.md](examples/MyAccountAuthenticationMethods.md#dpop).
|
|
202
254
|
|
|
255
|
+
### 10. Passwordless Authentication
|
|
256
|
+
|
|
257
|
+
Sign users in with a one-time code sent by email or SMS, or with a magic link sent by email, via [Auth0 embedded passwordless login](https://auth0.com/docs/authenticate/passwordless/implement-login/embedded-login/relevant-api-endpoints). OTP verification and the magic-link callback each establish a server-side session like every other login path. For prerequisites, both flows, custom scopes/audiences, step-up MFA, and error handling, see [examples/Passwordless.md](examples/Passwordless.md).
|
|
258
|
+
|
|
203
259
|
## Feedback
|
|
204
260
|
|
|
205
261
|
### Contributing
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import time
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
import jwt
|
|
6
|
+
|
|
7
|
+
from auth0_server_python.error import ConfigurationError
|
|
8
|
+
|
|
9
|
+
# RFC 7523 client-assertion type for private_key_jwt authentication.
|
|
10
|
+
CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
|
|
11
|
+
|
|
12
|
+
# Assertion lifetime in seconds.
|
|
13
|
+
_ASSERTION_LIFETIME = 60
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_client_assertion_key(private_key: Union[str, bytes], alg: str = "RS256") -> None:
|
|
17
|
+
"""
|
|
18
|
+
Verify the signing key and algorithm can produce a client assertion.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
private_key: The client's private signing key (PKCS8 PEM string or bytes).
|
|
22
|
+
alg: The signing algorithm (defaults to "RS256").
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ConfigurationError: If the key is malformed or does not match the algorithm.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
jwt.encode({"probe": True}, private_key, algorithm=alg)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise ConfigurationError(
|
|
31
|
+
f"Invalid client_assertion_signing_key for algorithm {alg}: {e}"
|
|
32
|
+
) from e
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_client_assertion(
|
|
36
|
+
private_key: Union[str, bytes],
|
|
37
|
+
client_id: str,
|
|
38
|
+
issuer: str,
|
|
39
|
+
alg: str = "RS256",
|
|
40
|
+
) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Mint a private_key_jwt client assertion for token-endpoint authentication (RFC 7523).
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
private_key: The client's private signing key (PKCS8 PEM string or bytes).
|
|
46
|
+
client_id: The Auth0 client ID, used as both the issuer and subject claim.
|
|
47
|
+
issuer: The authorization server issuer identifier, used as the audience claim.
|
|
48
|
+
alg: The signing algorithm (defaults to "RS256").
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The signed client-assertion JWT.
|
|
52
|
+
"""
|
|
53
|
+
now = int(time.time())
|
|
54
|
+
payload = {
|
|
55
|
+
"iss": client_id,
|
|
56
|
+
"sub": client_id,
|
|
57
|
+
"aud": issuer,
|
|
58
|
+
"iat": now,
|
|
59
|
+
"nbf": now,
|
|
60
|
+
"exp": now + _ASSERTION_LIFETIME,
|
|
61
|
+
"jti": secrets.token_urlsafe(32),
|
|
62
|
+
}
|
|
63
|
+
return jwt.encode(payload, private_key, algorithm=alg)
|
|
@@ -4,8 +4,10 @@ Handles Multi-Factor Authentication operations against the Auth0 MFA API.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
import ssl
|
|
7
8
|
import time
|
|
8
|
-
from
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
9
11
|
|
|
10
12
|
import httpx
|
|
11
13
|
|
|
@@ -28,6 +30,7 @@ from auth0_server_python.auth_types import (
|
|
|
28
30
|
)
|
|
29
31
|
from auth0_server_python.encryption.encrypt import decrypt, encrypt
|
|
30
32
|
from auth0_server_python.error import (
|
|
33
|
+
ConfigurationError,
|
|
31
34
|
DomainResolverError,
|
|
32
35
|
MfaChallengeError,
|
|
33
36
|
MfaEnrollmentError,
|
|
@@ -66,7 +69,15 @@ class MfaClient:
|
|
|
66
69
|
secret: str,
|
|
67
70
|
state_store=None,
|
|
68
71
|
state_identifier: str = "_a0_session",
|
|
69
|
-
headers: Optional[dict[str, str]] = None
|
|
72
|
+
headers: Optional[dict[str, str]] = None,
|
|
73
|
+
session_establisher: Optional[
|
|
74
|
+
Callable[..., Awaitable[None]]
|
|
75
|
+
] = None,
|
|
76
|
+
mfa_token_ttl: int = DEFAULT_MFA_TOKEN_TTL,
|
|
77
|
+
apply_client_authentication: Optional[Callable] = None,
|
|
78
|
+
use_mtls: bool = False,
|
|
79
|
+
ssl_context: Optional[ssl.SSLContext] = None,
|
|
80
|
+
token_endpoint_resolver: Optional[Callable[..., Awaitable[str]]] = None,
|
|
70
81
|
):
|
|
71
82
|
if callable(domain):
|
|
72
83
|
self._domain = None
|
|
@@ -80,12 +91,29 @@ class MfaClient:
|
|
|
80
91
|
self._state_store = state_store
|
|
81
92
|
self._state_identifier = state_identifier
|
|
82
93
|
self._headers = headers or {}
|
|
94
|
+
self._session_establisher = session_establisher
|
|
95
|
+
if mfa_token_ttl <= 0:
|
|
96
|
+
raise ConfigurationError("mfa_token_ttl must be a positive number of seconds")
|
|
97
|
+
self._mfa_token_ttl = mfa_token_ttl
|
|
98
|
+
self._apply_client_authentication = apply_client_authentication
|
|
99
|
+
self._use_mtls = use_mtls
|
|
100
|
+
self._ssl_context = ssl_context
|
|
101
|
+
self._token_endpoint_resolver = token_endpoint_resolver
|
|
83
102
|
|
|
84
103
|
def _get_http_client(self, **kwargs) -> httpx.AsyncClient:
|
|
85
104
|
"""Return an httpx.AsyncClient with default headers injected."""
|
|
86
105
|
headers = {**kwargs.pop("headers", {}), **self._headers}
|
|
106
|
+
if self._use_mtls and "verify" not in kwargs:
|
|
107
|
+
kwargs["verify"] = self._ssl_context
|
|
87
108
|
return httpx.AsyncClient(headers=headers, **kwargs)
|
|
88
109
|
|
|
110
|
+
def _apply_mfa_client_authentication(self, body: dict, base_url: str) -> None:
|
|
111
|
+
"""Add client authentication to an MFA request body (client_secret or client assertion)."""
|
|
112
|
+
if self._apply_client_authentication:
|
|
113
|
+
self._apply_client_authentication(body, f"{base_url}/", in_body=True)
|
|
114
|
+
elif self._client_secret:
|
|
115
|
+
body["client_secret"] = self._client_secret
|
|
116
|
+
|
|
89
117
|
async def _resolve_base_url(
|
|
90
118
|
self,
|
|
91
119
|
store_options: Optional[dict[str, Any]] = None
|
|
@@ -137,7 +165,7 @@ class MfaClient:
|
|
|
137
165
|
raise MfaTokenInvalidError()
|
|
138
166
|
|
|
139
167
|
elapsed = int(time.time()) - context.created_at
|
|
140
|
-
if elapsed >
|
|
168
|
+
if elapsed > self._mfa_token_ttl:
|
|
141
169
|
raise MfaTokenExpiredError()
|
|
142
170
|
|
|
143
171
|
return context
|
|
@@ -393,6 +421,7 @@ class MfaClient:
|
|
|
393
421
|
|
|
394
422
|
Raises:
|
|
395
423
|
MfaChallengeError: When the challenge fails.
|
|
424
|
+
ConfigurationError: If neither client_secret nor client_assertion_signing_key is configured.
|
|
396
425
|
"""
|
|
397
426
|
mfa_token = options.get("mfa_token")
|
|
398
427
|
if not mfa_token:
|
|
@@ -415,9 +444,9 @@ class MfaClient:
|
|
|
415
444
|
body: dict[str, Any] = {
|
|
416
445
|
"mfa_token": context.mfa_token,
|
|
417
446
|
"client_id": self._client_id,
|
|
418
|
-
"client_secret": self._client_secret,
|
|
419
447
|
"challenge_type": challenge_type
|
|
420
448
|
}
|
|
449
|
+
self._apply_mfa_client_authentication(body, base_url)
|
|
421
450
|
|
|
422
451
|
if "authenticator_id" in options and options["authenticator_id"]:
|
|
423
452
|
body["authenticator_id"] = options["authenticator_id"]
|
|
@@ -482,17 +511,28 @@ class MfaClient:
|
|
|
482
511
|
MfaVerifyError: When verification fails, or when dpop_key was supplied
|
|
483
512
|
but the server returned an unbound (Bearer) token.
|
|
484
513
|
MfaRequiredError: When chained MFA is required.
|
|
514
|
+
ConfigurationError: If dpop_key is combined with use_mtls. DPoP and mTLS
|
|
515
|
+
use incompatible token-binding mechanisms and cannot be used together.
|
|
516
|
+
ConfigurationError: If neither client_secret nor client_assertion_signing_key is configured.
|
|
485
517
|
"""
|
|
518
|
+
if self._use_mtls and dpop_key is not None:
|
|
519
|
+
raise ConfigurationError(
|
|
520
|
+
"dpop_key cannot be combined with use_mtls. DPoP and mTLS bind tokens "
|
|
521
|
+
"differently. DPoP would take precedence and the token would not be "
|
|
522
|
+
"certificate-bound."
|
|
523
|
+
)
|
|
486
524
|
mfa_token = options.get("mfa_token")
|
|
487
525
|
if not mfa_token:
|
|
488
526
|
raise MfaTokenInvalidError()
|
|
489
527
|
context = self.decrypt_mfa_token(mfa_token)
|
|
490
528
|
|
|
529
|
+
base_url = await self._resolve_base_url(store_options)
|
|
530
|
+
|
|
491
531
|
body: dict[str, Any] = {
|
|
492
532
|
"client_id": self._client_id,
|
|
493
|
-
"client_secret": self._client_secret,
|
|
494
533
|
"mfa_token": context.mfa_token
|
|
495
534
|
}
|
|
535
|
+
self._apply_mfa_client_authentication(body, base_url)
|
|
496
536
|
|
|
497
537
|
if "otp" in options:
|
|
498
538
|
body["grant_type"] = "http://auth0.com/oauth/grant-type/mfa-otp"
|
|
@@ -511,8 +551,10 @@ class MfaClient:
|
|
|
511
551
|
)
|
|
512
552
|
|
|
513
553
|
try:
|
|
514
|
-
|
|
515
|
-
|
|
554
|
+
if self._use_mtls and self._token_endpoint_resolver:
|
|
555
|
+
token_endpoint = await self._token_endpoint_resolver(store_options)
|
|
556
|
+
else:
|
|
557
|
+
token_endpoint = f"{base_url}/oauth/token"
|
|
516
558
|
|
|
517
559
|
async with self._get_http_client() as client:
|
|
518
560
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
@@ -626,6 +668,14 @@ class MfaClient:
|
|
|
626
668
|
)
|
|
627
669
|
|
|
628
670
|
if not state_data:
|
|
671
|
+
if self._session_establisher:
|
|
672
|
+
await self._session_establisher(
|
|
673
|
+
verify_response=verify_response,
|
|
674
|
+
audience=audience,
|
|
675
|
+
scope=scope,
|
|
676
|
+
store_options=store_options,
|
|
677
|
+
)
|
|
678
|
+
return
|
|
629
679
|
raise MfaVerifyError(
|
|
630
680
|
"No existing session found to update with MFA tokens"
|
|
631
681
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import ssl
|
|
2
3
|
from typing import TYPE_CHECKING, Optional
|
|
3
4
|
from urllib.parse import quote, unquote, urlparse
|
|
4
5
|
|
|
@@ -46,20 +47,31 @@ class MyAccountClient:
|
|
|
46
47
|
Client for interacting with the Auth0 MyAccount API.
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
|
-
def __init__(
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
domain: str,
|
|
53
|
+
headers: Optional[dict[str, str]] = None,
|
|
54
|
+
ssl_context: Optional[ssl.SSLContext] = None,
|
|
55
|
+
):
|
|
50
56
|
"""
|
|
51
57
|
Initialize the MyAccount API client.
|
|
52
58
|
|
|
53
59
|
Args:
|
|
54
60
|
domain: Auth0 domain (e.g., '<tenant>.<locality>.auth0.com')
|
|
55
61
|
headers: Optional default headers to include on every request
|
|
62
|
+
ssl_context: Optional SSL context for mTLS. When provided, the client
|
|
63
|
+
certificate is presented on every request so the My Account API can
|
|
64
|
+
verify cnf.x5t#S256 binding on cert-bound access tokens.
|
|
56
65
|
"""
|
|
57
66
|
self._domain = domain
|
|
58
67
|
self._headers = headers or {}
|
|
68
|
+
self._ssl_context = ssl_context
|
|
59
69
|
|
|
60
70
|
def _get_http_client(self, **kwargs) -> httpx.AsyncClient:
|
|
61
71
|
"""Return an httpx.AsyncClient with default headers injected."""
|
|
62
72
|
headers = {**kwargs.pop("headers", {}), **self._headers}
|
|
73
|
+
if self._ssl_context is not None and "verify" not in kwargs:
|
|
74
|
+
kwargs["verify"] = self._ssl_context
|
|
63
75
|
return httpx.AsyncClient(headers=headers, **kwargs)
|
|
64
76
|
|
|
65
77
|
@property
|