Repository navigation
feat: add passkey signup and login with identifier verification - #174
Merged
Merged
Conversation
Cover signin_with_passkey with a login assertion credential and rate-limit error propagation from passkey_login_challenge. Co-Authored-By: Claude <noreply@anthropic.com>
…anges auth0-server-python removed PasskeyErrorCode.TOO_MANY_REQUESTS and now reports rate limits through PasskeyError.status_code and retry_after. The exception handler read the removed attribute, so every PasskeyError raised AttributeError instead of rendering. It now maps status_code 429 to HTTP 429 and forwards retry_after as Retry-After. The passkey wrappers forward the new client_ip argument and match the upstream verification type. The passkey guide covers client_ip trust, rate limit retries, verification requirements and account existence. Co-Authored-By: Claude <noreply@anthropic.com>
The passkey wrappers use client_ip, PasskeyError.status_code and retry_after, and PasskeyDeliveryMethod, which first ship in 1.0.0b20. Earlier releases fail at import time. Raise the floor in pyproject.toml and requirements.txt and update the poetry.lock entry for the release. Co-Authored-By: Claude <noreply@anthropic.com>
rmad17
marked this pull request as ready for review
October 23, 2026 17:56
NandanPrabhu
previously approved these changes
Oct 24, 2026
nandan-bhat
reviewed
Oct 26, 2026
| status_code = 400 # Bad Request | ||
| elif isinstance(exc, AccessTokenForConnectionError): | ||
| status_code = 400 # Bad Request | ||
| elif isinstance(exc, PasskeyError) and exc.status_code == 429: |
There was a problem hiding this comment.
Question on the other PasskeyError cases. An Auth0 5xx, a gateway error or a network failure also reaches the browser as 400 (the table in the guide says the same). For the user, a temporary Auth0 outage then looks like a bad request.
Should we return 502 when status_code is None or 500 and above? I feel it is more correct than 400.
Contributor
Author
There was a problem hiding this comment.
I would agree that 400 is not correct status code but status code is the platforms call which SDK should not modify.
SigninRequest used PasskeyAuthResponse directly, which accepts unknown keys and has no length limits, so an oversized or padded credential was forwarded to Auth0 unchanged. Add strict AssertionData and AuthnResponse models with extra="forbid" and max_length, and build PasskeyAuthResponse from the validated body in the route. Co-Authored-By: Claude <noreply@anthropic.com>
request.client.host can hold unknown, an ip:port pair or a client-set X-Forwarded-For entry, and the helper passed it to Auth0 unchanged. Return the address only when ipaddress.ip_address accepts it. Reword the proxy advice to never use * for --forwarded-allow-ips, because uvicorn then trusts the first X-Forwarded-For entry, which the client sets. Co-Authored-By: Claude <noreply@anthropic.com>
The forwarded-for guidance named uvicorn flags as if every app used uvicorn. Say it applies when uvicorn is the server and tell readers of other servers to check which forwarding headers their server trusts. Co-Authored-By: Claude <noreply@anthropic.com>
signin_with_passkey does not take the Auth0Config audience or scope the way start_login does. Without openid in the scope no ID token comes back, so the guide route raised TypeError on state_data["user"], and without the audience get_access_token cannot find the stored token. Pass scope and audience in the sign-in route, handle a missing user, and stop describing the session as identical to /auth/callback. Co-Authored-By: Claude <noreply@anthropic.com>
Explain how /auth/login and signin_with_passkey differ instead of instructing the reader, and say why openid and the audience matter. Co-Authored-By: Claude <noreply@anthropic.com>
PasskeyLoginResult carries the access, refresh and ID tokens and its repr prints them. Say the route should keep it on the server and never return or log it. Co-Authored-By: Claude <noreply@anthropic.com>
The login section only described the response keys, so a copied null userHandle would fail validation. Add toLoginResponse, which builds the body from credential.toJSON() and leaves userHandle out when it is empty, and post the result to the shared sign-in route. Co-Authored-By: Claude <noreply@anthropic.com>
The identifier verification snippet used return and await at the top level, so it was not valid JavaScript as written. Wrap the flow in signUpWithPasskey so the return is legal and the snippet can be pasted and run. Co-Authored-By: Claude <noreply@anthropic.com>
A tenant that requires MFA got a bare 401 from the sign-in route and the guide said the SDK does not store the session, which is wrong for auth0-server-python 1.0.0b20. Catch MfaRequiredError in the sign-in route and return the mfa_token, add a verify route that calls mfa.verify with persist so the SDK creates the session, and note that StatefulStateStore cannot save the pending MFA state yet. Co-Authored-By: Claude <noreply@anthropic.com>
Run the real AuthClient and ServerClient against a mocked Auth0 so the tests catch wiring and release changes that the AsyncMock tests cannot. Cover sign-in stopping at mfa_required with an encrypted token, mfa.verify with persist creating the session, the raw token and one-time password grant sent to Auth0, and a rejected code leaving no session. Co-Authored-By: Claude <noreply@anthropic.com>
Show how to pass organization to the signup challenge, login challenge and sign-in calls and why the sign-in call matters. Replace the one-line DPoP pointer with notes on keeping the key per session on the server, passing it to mfa.verify, and the fact that get_access_token takes no dpop_key and sends no proof when it refreshes a token. Co-Authored-By: Claude <noreply@anthropic.com>
AuthClient never enables mTLS or Enterprise Connect, so ConfigurationError for dpop_key with mTLS and EnterpriseConnectError cannot reach a caller. Co-Authored-By: Claude <noreply@anthropic.com>
A PasskeyError from an Auth0 5xx, or from a failure with no HTTP response such as a network error or an unreadable body, reached the browser as 400, so a temporary Auth0 outage looked like a bad request. Map those to 502 like ApiError, keep 429 with Retry-After, and keep other Auth0 4xx answers as 400. Move the passkey mapping into a helper to stay under the handler's complexity limit and split the guide's error table row. Co-Authored-By: Claude <noreply@anthropic.com>
Reverts 222c468. Which HTTP status an app returns for an Auth0 outage or a network failure is the server's decision, so the handler only passes Auth0's own 429 through with Retry-After and leaves other PasskeyErrors at the default 400. Co-Authored-By: Claude <noreply@anthropic.com>
The AsyncMock tests only prove AuthClient calls a fake with the given keywords, so a renamed ServerClient keyword would pass them. Run the real AuthClient and ServerClient against a mocked Auth0 and check the challenge and token requests, the forwarded-for header, 429 handling, the wrong-code retry, organization claim checks, DPoP, and the stored session. Rename the file from test_passkey_mfa.py because it now covers more than MFA. Co-Authored-By: Claude <noreply@anthropic.com>
ruff S107 flags a string default on a parameter called token_type. Call it scheme, which is what the value is. Co-Authored-By: Claude <noreply@anthropic.com>
The guide only told readers to test the refresh path. Auth0's DPoP documentation says a refresh request for DPoP-bound tokens requires a proof made with the same key, and get_access_token takes no dpop_key, so it sends none. Say so, link the Auth0 section, and tell readers not to rely on get_access_token to refresh such a session. Co-Authored-By: Claude <noreply@anthropic.com>
The refresh bullet said Auth0 requires a DPoP proof on refresh, which does not hold for a confidential client such as this SDK. Keep what is verified, that get_access_token takes no dpop_key and sends no proof, and the instruction not to rely on it for a DPoP-bound session. Scope the PasskeyError check to signin_with_passkey, and note that mfa.verify only raises when a key is passed and the token comes back unbound. Co-Authored-By: Claude <noreply@anthropic.com>
A PasskeyError from an Auth0 5xx reached the browser as 400, so an outage looked like a bad request. Return the status Auth0 sent for any PasskeyError other than 429, which still adds Retry-After. Relay only 400 to 599, and keep 400 when there is no Auth0 response, such as a network failure. The status is Auth0's own, so no status is invented. Update the guide's error table and add tests for the relayed statuses. Co-Authored-By: Claude <noreply@anthropic.com>
nandan-bhat
approved these changes
Oct 26, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds passkey signup and login to
AuthClient, including identifier verification at signup. When a database connection requires email or phone verification, the signup challenge returnsverification_requiredandsignin_with_passkey()accepts the codes the user received.The new methods pass their arguments straight through to
ServerClientin auth0-server-python, following the existing custom token exchange methods. The SDK does not mount passkey routes. Apps add their own routes, as shown in the new guide.Changes
AuthClient(src/auth0_fastapi/auth/auth_client.py): three methods added at the end of the class. Each forwards every argument by keyword toServerClient.passkey_signup_challenge()acceptsuser_profile,connection,organization,user_metadata,delivery_method("text"or"voice"),client_ipandstore_options. The response includesverification_requiredwhen the connection requires verification.passkey_login_challenge()acceptsconnection,organization,client_ipandstore_options.signin_with_passkey()exchanges the credential for tokens and establishes the session for both signup and login. It accepts optionalverificationcodes,connection,organization,scope,audience,dpop_key,client_ipandstore_options.client_ipis forwarded as theauth0-forwarded-forheader. The SDK does not derive it from the request.src/auth0_fastapi/errors/__init__.py): re-exportsPasskeyErrorandPasskeyErrorCode.auth0_exception_handlerreturns aPasskeyErrorwithstatus_code429 as HTTP 429 and setsRetry-Afterwhen Auth0 sent one. Any otherPasskeyErrorthat carries an Auth0 status between 400 and 599 is returned with that same status. A failure with no Auth0 response, such as a network error, stays 400. The response body still carries onlyerrorandmessage, soauth_sessionnever appears in a generic error response.auth0-server-pythonto 1.0.0b20 inpyproject.toml,requirements.txtandpoetry.lock.examples/Passkeys.mdcovers:auth_sessionand codes as credentialsREADME.mdgains a Passkeys feature bullet and a Passkeys section that links to the guide.Testing
test_auth_client.py: newTestPasskeyclass with 11 tests. They cover:client_ipand a login credentialdpop_keypassed through as the same objectPasskeyErrorpropagation, withauth_sessionandverification_requiredintactstatus_code,retry_after) from both challengesInvalidArgumentErrorfor an unknown verification keytest_errors.py(new, 14 tests): 429 with and withoutRetry-After, Auth0 statuses relaybody leaves outauth_session, and a missing or non-error status staying at 400.test_passkey_http.py(new, 17 tests): runs the realAuthClientandServerClientagainst a mocked Auth0 (httpx.MockTransport), so a renamedServerClientkeyword fails them. They cover:auth0-forwarded-forheader and requestmfa_required,mfa.verifywithpersistcreating the session, and a rejected code leaving noneTested flows
organizationsetdpop_key, issuing DPoP-bound tokens