Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- **Fully Integrated Auth Flows**: Automatic routes for `/auth/login`, `/auth/logout`, `/auth/callback`, etc.
- **Session-Based**: Uses secure cookies to store user sessions, either stateless (all data in cookie) or stateful (data in a database).
- **Custom Token Exchange**: Exchange tokens from external identity providers or legacy systems for Auth0 tokens, with or without establishing a session.
- **Passkeys**: Sign users up and in with WebAuthn passkeys, including email and phone verification at signup.
- **Multiple Custom Domains (MCD)**: Support for applications using multiple custom domains on the same Auth0 tenant.
- **Account Linking**: Optional routes for linking multiple social or username/password accounts into a single Auth0 profile.
- **Backchannel Logout**: Receive logout tokens from Auth0 to invalidate sessions server-side.
Expand Down Expand Up @@ -319,6 +320,16 @@ Building on token exchange, the SDK also supports:

See [examples/CustomTokenExchange.md](./examples/CustomTokenExchange.md) for setup and code samples.

### Passkeys

Sign users up and in with [WebAuthn](https://www.w3.org/TR/webauthn-2/) passkeys (Touch ID, Face ID, Windows Hello, or a security key) via `passkey_signup_challenge()`, `passkey_login_challenge()` and `signin_with_passkey()`. Signing in establishes the same session as `/auth/callback`.

- **[Identifier verification](./examples/Passkeys.md#3-identifier-verification-optional)** - when your connection requires it, collect an email or phone code at signup and pass it to `signin_with_passkey()`.

Rate-limit the route that requests the signup challenge, because each call sends a code. See [Security considerations](./examples/Passkeys.md#4-security-considerations).

See [examples/Passkeys.md](./examples/Passkeys.md) for setup and code samples.

## Feedback

### Contributing
Expand Down
304 changes: 304 additions & 0 deletions examples/Passkeys.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ packages = [

[tool.poetry.dependencies]
python = ">=3.9"
auth0-server-python = ">=1.0.0b14"
auth0-server-python = ">=1.0.0b20"
fastapi = ">=0.115.11"
pydantic = "^2.12.5"

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
auth0-server-python>=1.0.0b14
auth0-server-python>=1.0.0b20
fastapi>=0.115.11
pydantic>=2.12.5
uvicorn>=0.39.0
Expand Down
145 changes: 144 additions & 1 deletion src/auth0_fastapi/auth/auth_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Imported from auth0-server-python
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional

from auth0_server_python.auth_server.server_client import ServerClient
from auth0_server_python.auth_types import (
Expand All @@ -9,6 +9,12 @@
LoginWithCustomTokenExchangeOptions,
LoginWithCustomTokenExchangeResult,
LogoutOptions,
PasskeyAuthResponse,
PasskeyDeliveryMethod,
PasskeyLoginChallengeResponse,
PasskeyLoginResult,
PasskeySignupChallengeResponse,
PasskeyUserProfile,
SessionTransferTokenResult,
StartInteractiveLoginOptions,
TokenExchangeResponse,
Expand All @@ -19,6 +25,9 @@
from auth0_fastapi.stores.cookie_transaction_store import CookieTransactionStore
from auth0_fastapi.stores.stateless_state_store import StatelessStateStore

if TYPE_CHECKING:
from jwcrypto import jwk


class AuthClient:
"""
Expand Down Expand Up @@ -358,3 +367,137 @@ def build_session_transfer_redirect(
return self.client.build_session_transfer_redirect(
target_login_url, result, organization=organization
)

async def passkey_signup_challenge(
self,
user_profile: Optional[PasskeyUserProfile] = None,
connection: Optional[str] = None,
organization: Optional[str] = None,
user_metadata: Optional[dict[str, Any]] = None,
delivery_method: Optional[PasskeyDeliveryMethod] = None,
client_ip: Optional[str] = None,
store_options: dict = None,
) -> PasskeySignupChallengeResponse:
"""
Requests a passkey registration challenge to start a signup.

Args:
user_profile: Profile of the new user (email, phone_number, name).
connection: Database connection name (optional).
organization: Organization ID or name (optional).
user_metadata: Custom metadata stored on the new user (optional).
delivery_method: Phone verification code channel, "text" or "voice" (optional).
client_ip: End user IP address, sent as the auth0-forwarded-for header (optional).
store_options: Optional options passed to the Transaction and State
Store. Only required when using Multiple Custom Domains, where
the domain resolver needs the incoming request.

Returns:
The PasskeySignupChallengeResponse (auth_session, authn_params_public_key,
verification_required).

Raises:
PasskeyError: If the challenge request fails or is rate limited
(status_code 429).
InvalidArgumentError: If delivery_method is not "text" or "voice".
"""
return await self.client.passkey_signup_challenge(
user_profile=user_profile,
connection=connection,
organization=organization,
user_metadata=user_metadata,
delivery_method=delivery_method,
client_ip=client_ip,
store_options=store_options,
)

async def passkey_login_challenge(
self,
connection: Optional[str] = None,
organization: Optional[str] = None,
client_ip: Optional[str] = None,
store_options: dict = None,
) -> PasskeyLoginChallengeResponse:
"""
Requests a passkey assertion challenge to start a login.

Args:
connection: Database connection name (optional).
organization: Organization ID or name (optional).
client_ip: End user IP address, sent as the auth0-forwarded-for header (optional).
store_options: Optional options passed to the Transaction and State
Store. Only required when using Multiple Custom Domains, where
the domain resolver needs the incoming request.

Returns:
The PasskeyLoginChallengeResponse (auth_session, authn_params_public_key).

Raises:
PasskeyError: If the challenge request fails or is rate limited
(status_code 429).
"""
return await self.client.passkey_login_challenge(
connection=connection,
organization=organization,
client_ip=client_ip,
store_options=store_options,
)

async def signin_with_passkey(
self,
auth_session: str,
authn_response: PasskeyAuthResponse,
verification: Optional[dict[str, Optional[str]]] = None,
connection: Optional[str] = None,
organization: Optional[str] = None,
scope: Optional[str] = None,
audience: Optional[str] = None,
dpop_key: Optional["jwk.JWK"] = None,
client_ip: Optional[str] = None,
store_options: dict = None,
) -> PasskeyLoginResult:
"""
Exchanges a passkey credential for tokens and establishes a session.

Args:
auth_session: The auth_session from the challenge, or from a retryable PasskeyError.
authn_response: The credential from navigator.credentials.create() or .get().
verification: Signup verification codes keyed by "email" or "phone" (optional).
connection: Database connection name (optional).
organization: Organization ID or name (optional).
scope: Space-delimited scopes (optional).
audience: API audience (optional).
dpop_key: EC P-256 key to bind the tokens with DPoP (optional).
client_ip: End user IP address, sent as the auth0-forwarded-for header (optional).
store_options: Options passed to the Transaction and State Store.
Must include {"request": request, "response": response} so the
session cookie can be written on response.

Returns:
The PasskeyLoginResult containing the session state (including the
resulting user).

Raises:
PasskeyError: If the token exchange fails or is rate limited
(status_code 429). See is_verification_retryable.
MfaRequiredError: If the tenant requires MFA after the passkey sign-in.
MissingRequiredArgumentError: If auth_session or authn_response is missing.
InvalidArgumentError: If verification is not a dict keyed by "email" or "phone".
ConfigurationError: If dpop_key is combined with mTLS.
ApiError: If the ID token fails validation.
IssuerValidationError: If the ID token issuer does not match the tenant.
OrganizationTokenValidationError: If the ID token's org claim does not match.
EnterpriseConnectError: If the client is configured for Enterprise Connect.
"""
return await self.client.signin_with_passkey(
auth_session=auth_session,
authn_response=authn_response,
verification=verification,
connection=connection,
organization=organization,
scope=scope,
audience=audience,
dpop_key=dpop_key,
client_ip=client_ip,
store_options=store_options,
)
8 changes: 8 additions & 0 deletions src/auth0_fastapi/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
IssuerValidationError,
MissingRequiredArgumentError,
MissingTransactionError,
PasskeyError,
PasskeyErrorCode,
)
from fastapi import Request
from fastapi.responses import JSONResponse
Expand All @@ -33,6 +35,7 @@ def auth0_exception_handler(request: Request, exc: Auth0Error):
"""
# Set a default status code
status_code = 400
headers = None

if isinstance(exc, MissingTransactionError):
status_code = 404 # Not Found
Expand All @@ -48,9 +51,14 @@ def auth0_exception_handler(request: Request, exc: Auth0Error):
status_code = 400 # Bad Request
elif isinstance(exc, AccessTokenForConnectionError):
status_code = 400 # Bad Request
elif isinstance(exc, PasskeyError) and exc.status_code == 429:
status_code = 429
if exc.retry_after is not None:
headers = {"Retry-After": str(exc.retry_after)}

return JSONResponse(
status_code=status_code,
headers=headers,
content={
"error": getattr(exc, "code", "auth_error"),
"message": exc.message or "An authentication error occurred.",
Expand Down
Loading
Loading