diff --git a/examples/ConnectedAccounts.md b/examples/ConnectedAccounts.md index 88f28f6..c7b13e8 100644 --- a/examples/ConnectedAccounts.md +++ b/examples/ConnectedAccounts.md @@ -27,6 +27,8 @@ server_client = ServerClient( ) ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + ## Login to the application Use the login methods to authenticate to the application and get a refresh token in order to use MRRT to call the My Account API. If you are also intending to authorize access to any custom API, you may optionally specify an audience and any relevant scopes but you must at a minimum include the `offline_access` scope to ensure you obtain a refresh token. diff --git a/examples/CustomTokenExchange.md b/examples/CustomTokenExchange.md index a334237..55d27c7 100644 --- a/examples/CustomTokenExchange.md +++ b/examples/CustomTokenExchange.md @@ -37,6 +37,8 @@ if response.id_token: print(f"ID Token: {response.id_token}") ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + ## 2. Login with Token Exchange Exchange a custom token AND establish a user session. diff --git a/examples/InteractiveLogin.md b/examples/InteractiveLogin.md index 02b359d..dea559e 100644 --- a/examples/InteractiveLogin.md +++ b/examples/InteractiveLogin.md @@ -23,6 +23,9 @@ server_client = ServerClient( } ) ``` + +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + Now call `start_interactive_login()` to obtain the authorization URL and redirect the user: ```python authorization_url = await server_client.start_interactive_login() diff --git a/examples/MFA.md b/examples/MFA.md index a878cc5..7da9254 100644 --- a/examples/MFA.md +++ b/examples/MFA.md @@ -124,6 +124,8 @@ except MfaRequiredError as error: mfa_token = context.mfa_token # Raw token for MFA API calls ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + > [!NOTE] > `get_access_token()` is not the only origin of `MfaRequiredError`. A passkey login (`signin_with_passkey`) raises the same error when a second factor is required — but there is **no session yet** at that point, which changes how you complete and persist the flow. See [Passkeys.md → Completing MFA on a passkey login](Passkeys.md#completing-mfa-on-a-passkey-login-and-where-the-session-comes-from). diff --git a/examples/MultipleCustomDomains.md b/examples/MultipleCustomDomains.md index ec6c68b..f106ab0 100644 --- a/examples/MultipleCustomDomains.md +++ b/examples/MultipleCustomDomains.md @@ -29,6 +29,8 @@ client = ServerClient( ) ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + ### Method 2: Dynamic Domain Resolver (MCD) For MCD support, provide a domain resolver function that receives a `DomainResolverContext`: diff --git a/examples/MutualTLS.md b/examples/MutualTLS.md index dba59b2..0b59cfe 100644 --- a/examples/MutualTLS.md +++ b/examples/MutualTLS.md @@ -43,6 +43,8 @@ auth0 = ServerClient( ) ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + The SDK passes `ssl_context` as `verify=ssl_context` to every `httpx.AsyncClient` it constructs, including the authlib client used for the authorization-code exchange. You never call `load_cert_chain` inside the SDK - the caller owns the TLS material. ## Mutual exclusion diff --git a/examples/OrganizationLogin.md b/examples/OrganizationLogin.md index 3d10eef..772fe75 100644 --- a/examples/OrganizationLogin.md +++ b/examples/OrganizationLogin.md @@ -34,6 +34,8 @@ authorization_url = await auth0.start_interactive_login( ) ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + `organization` accepts either an org ID (with the `org_` prefix) or a human-readable org name. The SDK validates the corresponding `org_id` or `org_name` claim in the returned ID token. ## Multi-Org Login diff --git a/examples/Passkeys.md b/examples/Passkeys.md index c1eed9a..95baa5c 100644 --- a/examples/Passkeys.md +++ b/examples/Passkeys.md @@ -31,14 +31,26 @@ A passkey ceremony is always **two steps**, because the WebAuthn signature happe ```python from auth0_server_python.auth_server.server_client import ServerClient +# Passkey sign-in persists a server-side session, and the challenge step stores +# short-lived transaction data, so ServerClient needs a state store and a +# transaction store (it does not create defaults). If your framework wrapper +# (e.g. auth0-fastapi) already provides these, reuse them. Otherwise, create +# your own implementations of the StateStore / TransactionStore ABCs. +state_store = YourStateStore(...) # or the store your framework provides +transaction_store = YourTransactionStore(...) # or the store your framework provides + server_client = ServerClient( domain="YOUR_CUSTOM_DOMAIN", client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET", secret="YOUR_SECRET", + state_store=state_store, + transaction_store=transaction_store, ) ``` +For store implementations (cookie, Redis, database) and how to pass `store_options`, see [examples/ConfigureStore.md](ConfigureStore.md). + The **Passkey** grant (`urn:okta:params:oauth:grant-type:webauthn`) must be enabled for your application under **Applications → Your App → Grant Types**. > [!NOTE] @@ -105,12 +117,11 @@ print(f"Signed up and logged in: {user['sub']}") ## 2. Passkey Login -Identical shape, different endpoints. The login challenge takes an optional `username` hint (for conditional UI), and the browser uses `navigator.credentials.get()`. +Identical shape, different endpoints. The browser uses `navigator.credentials.get()` and the user picks a passkey from the prompt. That credential identifies the user, so the login challenge takes no username. ```python # Step 1 — login challenge challenge = await server_client.passkey_login_challenge( - username="existing.user@example.com", # optional connection="Username-Password-Authentication", # optional store_options={"request": request, "response": response}, ) diff --git a/examples/Passwordless.md b/examples/Passwordless.md index dc35945..70650a6 100644 --- a/examples/Passwordless.md +++ b/examples/Passwordless.md @@ -62,6 +62,8 @@ server_client = ServerClient( ) ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + For apps using request/response-backed stores or multiple custom domains, pass `store_options={"request": request, "response": response}` to each method that reads or writes transaction/session state. ## 1. Email OTP diff --git a/examples/StepUpAuthentication.md b/examples/StepUpAuthentication.md index a5fc234..5f1984d 100644 --- a/examples/StepUpAuthentication.md +++ b/examples/StepUpAuthentication.md @@ -113,6 +113,8 @@ async def handle_callback(callback_url, request, response): return (result.get("app_state") or {}).get("returnTo", "/") ``` +`ServerClient` creates no default stores. Provide a `state_store` and a `transaction_store`. See [ConfigureStore.md](ConfigureStore.md). + > [!NOTE] > The redirect itself is framework-specific - these handlers return the URL to redirect to, and your app issues the actual HTTP redirect (e.g. a `302`/`303`). `max_age: 0` matters: without it, a user who authenticated moments ago may be returned straight to your callback without a fresh MFA prompt. diff --git a/src/auth0_server_python/auth_server/server_client.py b/src/auth0_server_python/auth_server/server_client.py index d8d3aa2..08c5664 100644 --- a/src/auth0_server_python/auth_server/server_client.py +++ b/src/auth0_server_python/auth_server/server_client.py @@ -3520,7 +3520,11 @@ async def passkey_login_challenge( then call signin_with_passkey() with the auth_session and credential result. Args: - username: Optional username hint for conditional UI. + username: Deprecated and ignored. Auth0 rejects ``username`` on the + passkey login challenge (the user is identified by the selected + credential, not by a supplied username), so it is never forwarded. + Retained for backward compatibility and will be removed in a future + major release. Passing it emits a DeprecationWarning. connection: Auth0 database connection name (realm). organization: Auth0 organization ID or name. store_options: Optional options for domain resolution. @@ -3532,6 +3536,15 @@ async def passkey_login_challenge( PasskeyError: If the challenge request fails. EnterpriseConnectError: If the client is configured for Enterprise Connect. """ + if username is not None: + warnings.warn( + "The 'username' argument to passkey_login_challenge is ignored: Auth0 " + "rejects 'username' on the passkey login challenge (the user is " + "identified by the selected credential). It is retained for backward " + "compatibility and will be removed in a future major release.", + DeprecationWarning, + stacklevel=2, + ) try: domain = await self._resolve_current_domain(store_options) @@ -3539,8 +3552,6 @@ async def passkey_login_challenge( body: dict[str, Any] = {"client_id": self._client_id} if self._client_secret: body["client_secret"] = self._client_secret - if username: - body["username"] = username if connection: body["realm"] = connection if resolved_org: diff --git a/src/auth0_server_python/tests/test_server_client.py b/src/auth0_server_python/tests/test_server_client.py index 77b43f5..040db50 100644 --- a/src/auth0_server_python/tests/test_server_client.py +++ b/src/auth0_server_python/tests/test_server_client.py @@ -7819,7 +7819,10 @@ async def test_passkey_login_challenge_minimal_body(mocker): @pytest.mark.asyncio -async def test_passkey_login_challenge_with_username(mocker): +async def test_passkey_login_challenge_ignores_username(mocker): + """username is deprecated and never forwarded: Auth0 rejects it on the login + challenge (passkey login is discoverable/usernameless). Passing it emits a + DeprecationWarning and leaves the request body unchanged.""" client = ServerClient( domain="auth0.local", client_id="test_client_id", @@ -7834,11 +7837,12 @@ async def test_passkey_login_challenge_with_username(mocker): mock_response.json = MagicMock(return_value=_PASSKEY_LOGIN_CHALLENGE_RESPONSE) mock_post.return_value = mock_response - await client.passkey_login_challenge(username="jane@example.com") + with pytest.warns(DeprecationWarning): + await client.passkey_login_challenge(username="jane@example.com") args, kwargs = mock_post.call_args body = kwargs["json"] - assert body["username"] == "jane@example.com" + assert "username" not in body @pytest.mark.asyncio