auth0-server-python 1.0.0b13__tar.gz → 1.0.0b15__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.0b13 → auth0_server_python-1.0.0b15}/PKG-INFO +41 -3
  2. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/README.md +40 -2
  3. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/pyproject.toml +1 -1
  4. auth0_server_python-1.0.0b15/src/auth0_server_python/auth_schemes/client_assertion.py +63 -0
  5. auth0_server_python-1.0.0b15/src/auth0_server_python/auth_server/__init__.py +6 -0
  6. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/auth_server/mfa_client.py +36 -6
  7. auth0_server_python-1.0.0b15/src/auth0_server_python/auth_server/passwordless_client.py +473 -0
  8. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/auth_server/server_client.py +502 -43
  9. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/auth_types/__init__.py +183 -4
  10. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/error/__init__.py +86 -21
  11. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/tests/test_mfa_client.py +97 -0
  12. auth0_server_python-1.0.0b15/src/auth0_server_python/tests/test_passwordless_client.py +1184 -0
  13. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/tests/test_server_client.py +1401 -245
  14. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/utils/helpers.py +27 -1
  15. auth0_server_python-1.0.0b13/src/auth0_server_python/auth_server/__init__.py +0 -5
  16. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/LICENSE +0 -0
  17. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/auth_schemes/__init__.py +0 -0
  18. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/auth_schemes/bearer_auth.py +0 -0
  19. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/auth_schemes/dpop_auth.py +0 -0
  20. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/auth_server/my_account_client.py +0 -0
  21. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/encryption/__init__.py +0 -0
  22. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/encryption/encrypt.py +0 -0
  23. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/store/__init__.py +0 -0
  24. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/store/abstract.py +0 -0
  25. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/telemetry.py +0 -0
  26. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/tests/test_dpop_auth.py +0 -0
  27. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/tests/test_my_account_client.py +0 -0
  28. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/tests/test_telemetry.py +0 -0
  29. {auth0_server_python-1.0.0b13 → auth0_server_python-1.0.0b15}/src/auth0_server_python/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auth0-server-python
3
- Version: 1.0.0b13
3
+ Version: 1.0.0b15
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,35 @@ 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
+
83
112
  ### 3. Add login to your Application (interactive)
84
113
 
85
114
  Before using redirect-based login, ensure the `redirect_uri` is configured when initializing the SDK:
@@ -172,6 +201,11 @@ response = await auth0.custom_token_exchange(
172
201
  print(response.access_token)
173
202
  ```
174
203
 
204
+ Building on token exchange, the SDK also supports:
205
+
206
+ - **[Delegation and Impersonation](examples/CustomTokenExchange.md#3-actor-tokens-delegation)** - exchange with an `actor_token` so the issued tokens record who is acting on whose behalf (the `act` claim).
207
+ - **[Impersonation via Session Transfer (STT)](examples/CustomTokenExchange.md#8-impersonation-via-session-transfer-stt)** - mint a Session Transfer Token to log an agent into a target app as a customer, via `request_session_transfer_token()` and `build_session_transfer_redirect()`.
208
+
175
209
  For more details and examples, see [examples/CustomTokenExchange.md](examples/CustomTokenExchange.md).
176
210
 
177
211
  ### 5. Multiple Custom Domains (MCD)
@@ -220,6 +254,10 @@ Let a logged-in user manage their own enrolled authentication methods — enroll
220
254
 
221
255
  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).
222
256
 
257
+ ### 10. Passwordless Authentication
258
+
259
+ 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).
260
+
223
261
  ## Feedback
224
262
 
225
263
  ### 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,35 @@ 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
+
58
87
  ### 3. Add login to your Application (interactive)
59
88
 
60
89
  Before using redirect-based login, ensure the `redirect_uri` is configured when initializing the SDK:
@@ -147,6 +176,11 @@ response = await auth0.custom_token_exchange(
147
176
  print(response.access_token)
148
177
  ```
149
178
 
179
+ Building on token exchange, the SDK also supports:
180
+
181
+ - **[Delegation and Impersonation](examples/CustomTokenExchange.md#3-actor-tokens-delegation)** - exchange with an `actor_token` so the issued tokens record who is acting on whose behalf (the `act` claim).
182
+ - **[Impersonation via Session Transfer (STT)](examples/CustomTokenExchange.md#8-impersonation-via-session-transfer-stt)** - mint a Session Transfer Token to log an agent into a target app as a customer, via `request_session_transfer_token()` and `build_session_transfer_redirect()`.
183
+
150
184
  For more details and examples, see [examples/CustomTokenExchange.md](examples/CustomTokenExchange.md).
151
185
 
152
186
  ### 5. Multiple Custom Domains (MCD)
@@ -195,6 +229,10 @@ Let a logged-in user manage their own enrolled authentication methods — enroll
195
229
 
196
230
  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).
197
231
 
232
+ ### 10. Passwordless Authentication
233
+
234
+ 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).
235
+
198
236
  ## Feedback
199
237
 
200
238
  ### Contributing
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "auth0-server-python"
3
- version = "1.0.0b13"
3
+ version = "1.0.0b15"
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"]
@@ -5,7 +5,8 @@ Handles Multi-Factor Authentication operations against the Auth0 MFA API.
5
5
 
6
6
  import json
7
7
  import time
8
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union
8
+ from collections.abc import Awaitable, Callable
9
+ from typing import TYPE_CHECKING, Any, Optional, Union
9
10
 
10
11
  import httpx
11
12
 
@@ -28,6 +29,7 @@ from auth0_server_python.auth_types import (
28
29
  )
29
30
  from auth0_server_python.encryption.encrypt import decrypt, encrypt
30
31
  from auth0_server_python.error import (
32
+ ConfigurationError,
31
33
  DomainResolverError,
32
34
  MfaChallengeError,
33
35
  MfaEnrollmentError,
@@ -66,7 +68,12 @@ class MfaClient:
66
68
  secret: str,
67
69
  state_store=None,
68
70
  state_identifier: str = "_a0_session",
69
- headers: Optional[dict[str, str]] = None
71
+ headers: Optional[dict[str, str]] = None,
72
+ session_establisher: Optional[
73
+ Callable[..., Awaitable[None]]
74
+ ] = None,
75
+ mfa_token_ttl: int = DEFAULT_MFA_TOKEN_TTL,
76
+ apply_client_authentication: Optional[Callable] = None,
70
77
  ):
71
78
  if callable(domain):
72
79
  self._domain = None
@@ -80,12 +87,24 @@ class MfaClient:
80
87
  self._state_store = state_store
81
88
  self._state_identifier = state_identifier
82
89
  self._headers = headers or {}
90
+ self._session_establisher = session_establisher
91
+ if mfa_token_ttl <= 0:
92
+ raise ConfigurationError("mfa_token_ttl must be a positive number of seconds")
93
+ self._mfa_token_ttl = mfa_token_ttl
94
+ self._apply_client_authentication = apply_client_authentication
83
95
 
84
96
  def _get_http_client(self, **kwargs) -> httpx.AsyncClient:
85
97
  """Return an httpx.AsyncClient with default headers injected."""
86
98
  headers = {**kwargs.pop("headers", {}), **self._headers}
87
99
  return httpx.AsyncClient(headers=headers, **kwargs)
88
100
 
101
+ def _apply_mfa_client_authentication(self, body: dict, base_url: str) -> None:
102
+ """Add client authentication to an MFA request body (client_secret or client assertion)."""
103
+ if self._apply_client_authentication:
104
+ self._apply_client_authentication(body, f"{base_url}/", in_body=True)
105
+ elif self._client_secret:
106
+ body["client_secret"] = self._client_secret
107
+
89
108
  async def _resolve_base_url(
90
109
  self,
91
110
  store_options: Optional[dict[str, Any]] = None
@@ -137,7 +156,7 @@ class MfaClient:
137
156
  raise MfaTokenInvalidError()
138
157
 
139
158
  elapsed = int(time.time()) - context.created_at
140
- if elapsed > DEFAULT_MFA_TOKEN_TTL:
159
+ if elapsed > self._mfa_token_ttl:
141
160
  raise MfaTokenExpiredError()
142
161
 
143
162
  return context
@@ -393,6 +412,7 @@ class MfaClient:
393
412
 
394
413
  Raises:
395
414
  MfaChallengeError: When the challenge fails.
415
+ ConfigurationError: If neither client_secret nor client_assertion_signing_key is configured.
396
416
  """
397
417
  mfa_token = options.get("mfa_token")
398
418
  if not mfa_token:
@@ -415,9 +435,9 @@ class MfaClient:
415
435
  body: dict[str, Any] = {
416
436
  "mfa_token": context.mfa_token,
417
437
  "client_id": self._client_id,
418
- "client_secret": self._client_secret,
419
438
  "challenge_type": challenge_type
420
439
  }
440
+ self._apply_mfa_client_authentication(body, base_url)
421
441
 
422
442
  if "authenticator_id" in options and options["authenticator_id"]:
423
443
  body["authenticator_id"] = options["authenticator_id"]
@@ -482,17 +502,20 @@ class MfaClient:
482
502
  MfaVerifyError: When verification fails, or when dpop_key was supplied
483
503
  but the server returned an unbound (Bearer) token.
484
504
  MfaRequiredError: When chained MFA is required.
505
+ ConfigurationError: If neither client_secret nor client_assertion_signing_key is configured.
485
506
  """
486
507
  mfa_token = options.get("mfa_token")
487
508
  if not mfa_token:
488
509
  raise MfaTokenInvalidError()
489
510
  context = self.decrypt_mfa_token(mfa_token)
490
511
 
512
+ base_url = await self._resolve_base_url(store_options)
513
+
491
514
  body: dict[str, Any] = {
492
515
  "client_id": self._client_id,
493
- "client_secret": self._client_secret,
494
516
  "mfa_token": context.mfa_token
495
517
  }
518
+ self._apply_mfa_client_authentication(body, base_url)
496
519
 
497
520
  if "otp" in options:
498
521
  body["grant_type"] = "http://auth0.com/oauth/grant-type/mfa-otp"
@@ -511,7 +534,6 @@ class MfaClient:
511
534
  )
512
535
 
513
536
  try:
514
- base_url = await self._resolve_base_url(store_options)
515
537
  token_endpoint = f"{base_url}/oauth/token"
516
538
 
517
539
  async with self._get_http_client() as client:
@@ -626,6 +648,14 @@ class MfaClient:
626
648
  )
627
649
 
628
650
  if not state_data:
651
+ if self._session_establisher:
652
+ await self._session_establisher(
653
+ verify_response=verify_response,
654
+ audience=audience,
655
+ scope=scope,
656
+ store_options=store_options,
657
+ )
658
+ return
629
659
  raise MfaVerifyError(
630
660
  "No existing session found to update with MFA tokens"
631
661
  )