Skip to content

Commit dbafc77

Browse files
committed
Merge branch 'feat/passwordless-support-ca' of github.com:auth0/auth0-server-python into feat/passwordless-support-ca
# Conflicts: # src/auth0_server_python/error/__init__.py
2 parents 9a0305d + cfe2900 commit dbafc77

6 files changed

Lines changed: 12 additions & 58 deletions

File tree

examples/Passwordless.md

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Passwordless lets users sign in with a one-time code sent by email or SMS, or with a magic link sent by email. This guide covers the **embedded login** flow on `ServerClient.passwordless` and how each path establishes a server-side session.
44

55
> [!NOTE]
6-
> Passwordless API flows use Auth0 Legacy Passwordless connections (`email` and `sms`). Enable the **Passwordless OTP** grant for your application. See the [Auth0 Passwordless API documentation](https://auth0.com/docs/authenticate/passwordless/implement-login/embedded-login/relevant-api-endpoints).
6+
> Passwordless API flows use Auth0 Passwordless connections (`email` and `sms`). Enable the **Passwordless OTP** grant for your application. See the [Auth0 Passwordless API documentation](https://auth0.com/docs/authenticate/passwordless/implement-login/embedded-login/relevant-api-endpoints).
77
88
> [!IMPORTANT]
99
> These flows are for confidential server-side applications. Tokens stay on the server. The browser should only receive your application's session cookie or opaque session reference.
@@ -35,36 +35,20 @@ OTP start does **not** create a session. The session exists only after `verify()
3535
## Prerequisites
3636

3737
These flows require a **Regular Web Application**. Passwordless token exchange
38-
needs a client secret, which a public/SPA client cannot hold safely.
38+
needs a client secret, which a public client cannot hold safely.
3939

40-
Two tenant-level settings are also required and are easy to miss because
41-
neither failure mode looks like a configuration problem:
40+
Two tenant-level settings are also required:
4241

4342
1. **Authentication Profile must be "Identifier First."** The default
4443
"Universal Login" profile blocks the direct `/oauth/token` call this SDK
4544
uses for OTP verification. Without it, OTP `verify()` fails with
4645
`unauthorized_client`. Set it under your tenant's Authentication Profile
4746
settings. (Skip this if you only use passwordless via Universal Login
4847
redirects rather than this SDK's embedded flow.)
49-
2. **Enable the Passwordless OTP grant type** on your application
50-
(**Applications -> Your App -> Advanced Settings -> Grant Types**). Without
48+
2. **Enable the Passwordless OTP grant type** on your application. Without
5149
it, OTP verification also fails with `unauthorized_client`.
5250
3. **Magic link only.** Set the tenant flag
53-
`universal_login.passwordless.allow_magiclink_verify_without_session` to
54-
`true` via the Management API:
55-
56-
```
57-
PATCH /api/v2/tenants/settings
58-
{ "universal_login": { "passwordless": { "allow_magiclink_verify_without_session": true } } }
59-
```
60-
61-
This is required for **any** server-side SDK completing magic link (this
62-
one, Express, Next.js, etc.). The browser that opens the emailed link is
63-
not guaranteed to be the same browser/session that started the flow.
64-
Without it, the user sees: *"The link must be opened on the same device
65-
and browser from which you submitted your email address."* This flag is
66-
not documented in the public Auth0 API reference, so if you don't set it
67-
here you will not discover it from a 400 error message.
51+
`universal_login.passwordless.allow_magiclink_verify_without_session` to `true`.
6852

6953
```python
7054
from auth0_server_python.auth_server.server_client import ServerClient
@@ -96,7 +80,6 @@ start_result = await server_client.passwordless.start(
9680
store_options={"request": request, "response": response},
9781
)
9882

99-
# start_result.id is Auth0's request identifier when returned by the API.
10083
```
10184

10285
### Step 2 - Verify the code and establish the session
@@ -297,7 +280,7 @@ except MfaRequiredError as e:
297280
```
298281

299282
> [!NOTE]
300-
> Passwordless OTP MFA is like passkey-first MFA: there is no existing application session until MFA verification succeeds. `persist=True` creates the initial SDK session when the MFA response includes an ID token.
283+
> For Passwordless OTP MFA there is no existing application session until MFA verification succeeds. `persist=True` creates the initial SDK session when the MFA response includes an ID token.
301284
302285
## Error Handling
303286

references/docs-update.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,4 @@ it from `README.md`'s section for the feature. Name the file after the flow, mat
6363
| CIBA | `examples/ClientInitiatedBackChannelLogin.md` |
6464
| MCD domain resolver | `examples/MultipleCustomDomains.md` |
6565
| Account linking / unlinking | `examples/UserLinking.md` |
66+
| Passwordless email/SMS OTP + magic link | `examples/Passwordless.md` |

src/auth0_server_python/auth_server/passwordless_client.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"""
22
Passwordless Client for auth0-server-python SDK.
33
4-
Implements embedded passwordless login (Legacy Passwordless connections) for a
5-
confidential (Regular Web App) client:
4+
Implements embedded passwordless login for a client:
65
76
* Email OTP / SMS OTP - ``start()`` sends a code, ``verify()`` exchanges it for
87
tokens via the passwordless-OTP grant and establishes a server-side session.
98
* Magic link - ``start(send="link")`` emails a one-click link. Completion is
109
handled by the standard callback (``ServerClient.complete_interactive_login``),
1110
not by ``verify()``.
1211
13-
Tokens never leave the server. The browser holds only the opaque session
14-
reference (RWA / BFF posture).
12+
Tokens never leave the server. The browser holds only the opaque session.
1513
"""
1614

1715
from typing import TYPE_CHECKING, Any, Optional
@@ -45,10 +43,7 @@
4543
PASSWORDLESS_OTP_GRANT_TYPE = "http://auth0.com/oauth/grant-type/passwordless/otp"
4644
DEFAULT_PASSWORDLESS_EMAIL_SCOPE = "openid profile email"
4745
DEFAULT_PASSWORDLESS_SMS_SCOPE = "openid profile"
48-
# Header Auth0 reads for the real end-user IP (confidential clients with
49-
# "Trust Token Endpoint IP Header" enabled).
5046
FORWARDED_FOR_HEADER = "auth0-forwarded-for"
51-
# Cap on a non-JSON error body retained as an exception cause.
5247
_RAW_ERROR_BODY_LIMIT = 2048
5348

5449

@@ -57,8 +52,7 @@ class PasswordlessClient:
5752
Client for Auth0 embedded passwordless operations.
5853
5954
Composes the parent :class:`ServerClient` to reuse domain resolution, OIDC
60-
discovery, JWKS/ID-token verification, and session persistence rather than
61-
duplicating that security-critical logic.
55+
discovery, JWKS/ID-token verification, and session persistence.
6256
"""
6357

6458
def __init__(self, server_client: "ServerClient"):
@@ -333,22 +327,11 @@ def _prepare_magic_link_start(
333327
if store_options is None:
334328
raise MissingRequiredArgumentError("store_options")
335329

336-
# Auth0 echoes `state` back unvalidated on this flow. It does not
337-
# compare it server-side, and the clicked link's query string can
338-
# overwrite whatever was originally stored. This SDK's single-use,
339-
# state-keyed transaction plus the exact-match, SDK-owned
340-
# `redirect_uri` is therefore the only CSRF and authorization-code-
341-
# interception control on magic link. The server provides none.
342-
# Never make `state`/`redirect_uri` caller-overridable.
343330
state = PKCE.generate_random_string(32)
344331
auth_params["redirect_uri"] = redirect_uri
345332
auth_params["response_type"] = "code"
346333
auth_params["state"] = state
347334
auth_params.setdefault("scope", DEFAULT_PASSWORDLESS_EMAIL_SCOPE)
348-
# A caller-supplied scope replaces the default wholesale, so `openid`
349-
# is re-injected rather than trusted: without it Auth0 returns no ID
350-
# token and the callback never demands one, leaving a session with no
351-
# signature-verified claims.
352335
auth_params["scope"] = self._ensure_openid_scope(auth_params["scope"])
353336

354337
transaction_data = TransactionData(
@@ -481,7 +464,7 @@ def _raw_text(response) -> Optional[str]:
481464
error body.
482465
483466
Capped because the body may be an HTML error page, WAF block page, or
484-
proxy dump: it is attached as the exception ``cause`` and reaches any
467+
proxy dump: it is attached as the exception `cause` and reaches any
485468
logger that serializes it, and httpx applies no response-size limit.
486469
"""
487470
try:

src/auth0_server_python/auth_server/server_client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,6 @@ async def complete_interactive_login(
872872
if transaction_data.app_state:
873873
result["app_state"] = transaction_data.app_state
874874

875-
# For RAR
876875
authorization_details = token_response.get("authorization_details")
877876
if authorization_details:
878877
result["authorization_details"] = authorization_details

src/auth0_server_python/auth_types/__init__.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -917,14 +917,8 @@ class _StartPasswordlessBase(BaseModel):
917917
# BCP 47 tag (e.g. "fr", "en-US"). Forwarded as x-request-language to
918918
# localise the email/SMS template.
919919
language: Optional[str] = None
920-
# Extra params forwarded to /passwordless/start. SDK-owned keys
921-
# (PASSWORDLESS_RESERVED_AUTH_PARAMS) are stripped in the client.
922920
auth_params: Optional[dict[str, Any]] = None
923-
# Attempted solution to a captcha challenge, when the tenant requires one.
924921
captcha: Optional[str] = None
925-
# End-user client IP, relayed to Auth0 as `auth0-forwarded-for` so brute-force
926-
# and suspicious-IP protection key on the real user, not the app server. Only
927-
# honored for confidential clients with "Trust Token Endpoint IP Header" on.
928922
client_ip: Optional[str] = None
929923

930924
@field_validator("language")
@@ -975,16 +969,11 @@ class VerifyPasswordlessOtpOptions(BaseModel):
975969
model_config = ConfigDict(extra="forbid")
976970

977971
connection: PasswordlessConnection
978-
# Sent to Auth0 as the `otp` form parameter.
979972
verification_code: str
980973
email: Optional[str] = None
981974
phone_number: Optional[str] = None
982975
scope: Optional[str] = None
983976
audience: Optional[str] = None
984-
# End-user client IP, relayed to Auth0 as `auth0-forwarded-for` on the OTP
985-
# token exchange so brute-force protection keys on the real user, not the
986-
# app server. Honored only for confidential clients with "Trust Token
987-
# Endpoint IP Header" enabled.
988977
client_ip: Optional[str] = None
989978

990979
@field_validator("phone_number")
@@ -1019,7 +1008,6 @@ def username(self) -> str:
10191008
class PasswordlessStartResult(BaseModel):
10201009
"""Success payload from POST /passwordless/start."""
10211010

1022-
# Auth0 returns the request id as `_id`. Aliased so `.id` is populated.
10231011
id: Optional[str] = Field(default=None, alias="_id")
10241012

10251013
class Config:

src/auth0_server_python/error/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Error classes for the auth0-server-python SDK.
2+
Error classes for the SDK.
33
These exceptions provide specific error types for different failure scenarios.
44
"""
55

0 commit comments

Comments
 (0)