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.
Files changed (29) hide show
  1. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/PKG-INFO +59 -3
  2. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/README.md +58 -2
  3. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/pyproject.toml +1 -1
  4. auth0_server_python-1.0.0b16/src/auth0_server_python/auth_schemes/client_assertion.py +63 -0
  5. auth0_server_python-1.0.0b16/src/auth0_server_python/auth_server/__init__.py +6 -0
  6. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_server/mfa_client.py +57 -7
  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
  8. auth0_server_python-1.0.0b16/src/auth0_server_python/auth_server/passwordless_client.py +478 -0
  9. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_server/server_client.py +421 -53
  10. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_types/__init__.py +165 -4
  11. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/error/__init__.py +83 -21
  12. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_mfa_client.py +161 -0
  13. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_my_account_client.py +22 -0
  14. auth0_server_python-1.0.0b16/src/auth0_server_python/tests/test_passwordless_client.py +1300 -0
  15. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_server_client.py +1345 -68
  16. auth0_server_python-1.0.0b14/src/auth0_server_python/auth_server/__init__.py +0 -5
  17. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/LICENSE +0 -0
  18. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
  19. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
  20. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/auth_schemes/dpop_auth.py +0 -0
  21. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/encryption/__init__.py +0 -0
  22. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/encryption/encrypt.py +0 -0
  23. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/store/__init__.py +0 -0
  24. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/store/abstract.py +0 -0
  25. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/telemetry.py +0 -0
  26. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_dpop_auth.py +0 -0
  27. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/tests/test_telemetry.py +0 -0
  28. {auth0_server_python-1.0.0b14 → auth0_server_python-1.0.0b16}/src/auth0_server_python/utils/__init__.py +0 -0
  29. {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.0b14
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 install auth0-server-python
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 install auth0-server-python
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-server-python"
3
- version = "1.0.0b14"
3
+ version = "1.0.0b16"
4
4
  description = "Auth0 server-side Python SDK"
5
5
  readme = "README.md"
6
6
  authors = ["Auth0 <support@okta.com>"]
@@ -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)
@@ -0,0 +1,6 @@
1
+ from .mfa_client import MfaClient
2
+ from .my_account_client import MyAccountClient
3
+ from .passwordless_client import PasswordlessClient
4
+ from .server_client import ServerClient
5
+
6
+ __all__ = ["ServerClient", "MyAccountClient", "MfaClient", "PasswordlessClient"]
@@ -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 typing import TYPE_CHECKING, Any, Callable, Optional, Union
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 > DEFAULT_MFA_TOKEN_TTL:
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
- base_url = await self._resolve_base_url(store_options)
515
- token_endpoint = f"{base_url}/oauth/token"
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__(self, domain: str, headers: Optional[dict[str, str]] = None):
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