Skip to content
Merged
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ For more details and examples, see [examples/RetrievingData.md](examples/Retriev

Sign users up or in with [WebAuthn](https://www.w3.org/TR/webauthn-2/) passkeys (Touch ID, Face ID, Windows Hello, or a security key) instead of a password, via [Auth0 passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys). The ceremony is two steps β€” request a challenge, sign it in the browser, then complete sign-in β€” and establishes a server-side session like every other login path. For the signup and login flows, organizations, step-up MFA, and error handling, see [examples/Passkeys.md](examples/Passkeys.md).

### 8. My Account API β€” Authentication Methods

Let a logged-in user manage their own enrolled authentication methods β€” enroll a new passkey (or other factor), list, rename, and delete β€” via the [My Account API](https://auth0.com/docs/manage-users/my-account-api). For obtaining a scoped token, the enroll/verify ceremony, listing, updating, deleting, and error handling, see [examples/MyAccountAuthenticationMethods.md](examples/MyAccountAuthenticationMethods.md).

### 9. DPoP β€” Sender-Constrained Tokens (Passkeys & MyAccount)

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).

## Feedback

### Contributing
Expand Down
22 changes: 22 additions & 0 deletions examples/MFA.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The Auth0 MFA API allows you to manage multi-factor authentication for users in
- [Verify with OTP](#verify-with-otp)
- [Verify with Recovery Code](#verify-with-recovery-code)
- [Verify with Push Notification (Polling)](#verify-with-push-notification-polling)
- [Verify with DPoP (sender-constrained tokens)](#verify-with-dpop-sender-constrained-tokens)
- [Session Persistence](#session-persistence)
- [Automatic Session Update](#automatic-session-update)
- [Manual Session Update](#manual-session-update)
Expand Down Expand Up @@ -519,6 +520,27 @@ async def poll_push_verification(server_client, mfa_token, oob_code, timeout=60)
> [!NOTE]
> When polling for push notification approval, the API returns an `authorization_pending` error until the user approves or denies the request. A `slow_down` error indicates you should increase the polling interval.

### Verify with DPoP (sender-constrained tokens)

When the login that triggered MFA was DPoP-bound (for example a `signin_with_passkey(dpop_key=...)` that returned `MfaRequiredError`), pass the **same** `dpop_key` to `verify()` so the token minted by the MFA step-up stays sender-constrained:

```python
verify_response = await server_client.mfa.verify(
{
"mfa_token": mfa_token,
"otp": "123456",
},
dpop_key=dpop_key, # the same EC P-256 key the login was bound to
)

assert verify_response.token_type == "DPoP"
```

The SDK does not store your private key, so you must re-supply it on the `verify()` call. It attaches a token-endpoint DPoP proof, transparently handles the server-nonce challenge, and **rejects a Bearer downgrade** β€” if `dpop_key` was supplied but the server returned an unbound token (or vice versa), `verify()` raises `MfaVerifyError` instead of silently dropping the sender constraint.

> [!WARNING]
> The `dpop_key` is a **Tier 0 secret**. Keep it in your secret store, never log it, use one key per user/session, and use **EC P-256 only**.

## Session Persistence

By default, `verify()` returns tokens without persisting them to the session store. However, you can automatically persist tokens by setting `persist=True`.
Expand Down
241 changes: 241 additions & 0 deletions examples/MyAccountAuthenticationMethods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# My Account API β€” Authentication Methods & Factors

The [My Account API](https://auth0.com/docs/manage-users/my-account-api) lets a **logged-in user manage their own account**. This guide covers the **authentication-methods** and **factors** surface: enrolling a new passkey (or other factor), and listing, reading, renaming, and deleting a user's enrolled methods.

> [!NOTE]
> This is a different My Account resource from [Connected Accounts](ConnectedAccounts.md) (Token Vault). Connected-accounts management is exposed as convenience methods on `ServerClient`; **authentication-method management is on `MyAccountClient` directly**, because each call takes a user access token you obtain yourself. The two share the same My Account setup (activation, MRRT, scopes, `MyAccountApiError`) β€” see [ConnectedAccounts.md β†’ Pre-requisites](ConnectedAccounts.md#pre-requisites) for that common setup.

> [!NOTE]
> To **sign in** with a passkey (rather than manage one), see [examples/Passkeys.md](Passkeys.md). To **bind these calls to a held key**, pass an optional `dpop_key` β€” see [DPoP](#dpop) below.

## Table of Contents

- [Prerequisites](#prerequisites)
- [Obtaining a scoped token](#obtaining-a-scoped-token)
- [1. List factors available for enrollment](#1-list-factors-available-for-enrollment)
- [2. Enroll an authentication method (passkey)](#2-enroll-an-authentication-method-passkey)
- [3. List authentication methods](#3-list-authentication-methods)
- [4. Get a single authentication method](#4-get-a-single-authentication-method)
- [5. Update (rename) an authentication method](#5-update-rename-an-authentication-method)
- [6. Delete an authentication method](#6-delete-an-authentication-method)
- [DPoP](#dpop)
- [Error Handling](#error-handling)

## Prerequisites

1. [Activate the My Account API](https://auth0.com/docs/manage-users/my-account-api#activate-the-my-account-api) on your tenant and enable access for your application.
2. [Configure MRRT](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token) so your refresh-token policy can mint tokens for the My Account audience (`https://{yourDomain}/me/`) with the authentication-methods scopes.
3. Passkey enrollment additionally requires a [Custom Domain](https://auth0.com/docs/customize/custom-domains) and the native passkey feature on your tenant.

See [Auth0 Docs β†’ Scope](https://auth0.com/docs/manage-users/my-account-api#scope) for the full scope reference. The scopes for this surface (note the **hyphens**):

| Operation | Scope |
|-----------|-------|
| List factors | `read:me:factors` |
| List / get methods | `read:me:authentication-methods` |
| Enroll / verify | `create:me:authentication-methods` |
| Update | `update:me:authentication-methods` |
| Delete | `delete:me:authentication-methods` |

> [!TIP]
> As with Connected Accounts, set the default `scope` for the My Account audience when constructing `ServerClient` to avoid a fresh token request per scope. See [ConnectedAccounts.md β†’ A note about scopes](ConnectedAccounts.md#a-note-about-scopes).

## Obtaining a scoped token

`MyAccountClient` is **stateless** β€” it takes a correctly-scoped user access token on every call. Obtain that token from your `ServerClient` session via MRRT, then construct the client. See [Auth0 Docs β†’ Get an access token](https://auth0.com/docs/manage-users/my-account-api#get-an-access-token) for the underlying token requirements.

```python
from auth0_server_python.auth_server.my_account_client import MyAccountClient

# Fresh My Account-scoped token for the current session (MRRT exchange)
access_token = await server_client.get_access_token(
store_options={"request": request, "response": response},
audience=f"https://{YOUR_CUSTOM_DOMAIN}/me/",
scope="create:me:authentication-methods read:me:authentication-methods read:me:factors",
)

my_account = MyAccountClient(domain=YOUR_CUSTOM_DOMAIN)
```

## 1. List factors available for enrollment

```python
factors = await my_account.get_factors(access_token=access_token)
for factor in factors.factors:
print(factor.type, factor.usage)
```

## 2. Enroll an authentication method (passkey)

Enrollment is a **two-step** ceremony, mirroring sign-in: request a challenge, sign it in the browser, then verify. See [Auth0 Docs β†’ Enrollment flow](https://auth0.com/docs/manage-users/my-account-api#enrollment-flow) (and, for passkeys specifically, [Embedded login with native passkeys](https://auth0.com/docs/manage-users/my-account-api#embedded-login-with-native-passkeys)) for the platform-level description of this ceremony.

### Step 1 β€” Start enrollment

```python
from auth0_server_python.auth_types import EnrollAuthenticationMethodRequest

challenge = await my_account.enroll_authentication_method(
access_token=access_token,
request=EnrollAuthenticationMethodRequest(type="passkey"),
)

# challenge.authentication_method_id -> id of the new (unverified) method
# challenge.auth_session -> Tier 1 session credential (do not log)
# challenge.authn_params_public_key -> pass to navigator.credentials.create()
```

`EnrollAuthenticationMethodRequest.type` is a closed set: `passkey`, `email`, `phone`, `totp`, `push-notification`, `recovery-code`, `password`. For non-passkey types, supply the relevant fields (`email`, `phone_number`, `preferred_authentication_method`). An invalid type fails at construction with a clear `ValidationError`.

### Step 2 β€” Create the credential in the browser

Pass `challenge.authn_params_public_key` to `navigator.credentials.create()` and collect the resulting credential.

### Step 3 β€” Verify enrollment

```python
from auth0_server_python.auth_types import (
VerifyAuthenticationMethodRequest,
PasskeyAuthResponse,
)

method = await my_account.verify_authentication_method(
access_token=access_token,
authentication_method_id=challenge.authentication_method_id,
request=VerifyAuthenticationMethodRequest(
auth_session=challenge.auth_session,
authn_response=PasskeyAuthResponse(
id=credential["id"],
raw_id=credential["rawId"],
type="public-key",
response={
"clientDataJSON": credential["response"]["clientDataJSON"],
"attestationObject": credential["response"]["attestationObject"],
},
),
),
)
print(f"Enrolled: {method.id} ({method.type})")
```

> [!NOTE]
> For non-passkey types, set the matching field on `VerifyAuthenticationMethodRequest` instead of `authn_response`: `otp_code` (email/phone/totp), `recovery_code`, or `password`. A push enrollment needs only `auth_session`.

## 3. List authentication methods

See [Auth0 Docs β†’ List authentication methods](https://auth0.com/docs/manage-users/my-account-api#list-authentication-methods).

```python
all_methods = await my_account.list_authentication_methods(access_token=access_token)

# Filter by type
passkeys = await my_account.list_authentication_methods(
access_token=access_token,
type_filter="passkey",
)
for m in passkeys.authentication_methods:
print(m.id, m.type, m.created_at)
```

> [!NOTE]
> `AuthenticationMethod` and `Factor` are forward-tolerant (`extra="allow"`): fields or method/factor types Auth0 adds later still deserialize. Don't switch exhaustively on `type` β€” handle unknown types gracefully.

## 4. Get a single authentication method

```python
method = await my_account.get_authentication_method(
access_token=access_token,
authentication_method_id="passkey|abc123",
)
```

> [!NOTE]
> Method IDs (e.g. `passkey|abc123`) can contain characters like `|`. The SDK URL-encodes every ID it places in a path, so pass the raw ID exactly as returned β€” do not pre-encode it.

## 5. Update (rename) an authentication method

```python
from auth0_server_python.auth_types import UpdateAuthenticationMethodRequest

method = await my_account.update_authentication_method(
access_token=access_token,
authentication_method_id="totp|123",
request=UpdateAuthenticationMethodRequest(name="My Authenticator App"),
)
```

## 6. Delete an authentication method

See [Auth0 Docs β†’ Delete an authentication method](https://auth0.com/docs/manage-users/my-account-api#delete-an-authentication-method).

```python
await my_account.delete_authentication_method(
access_token=access_token,
authentication_method_id="passkey|abc123",
)
# Returns None on success (HTTP 204).
```

## DPoP

Every method above accepts an optional `dpop_key` to present a sender-constrained token (`Authorization: DPoP` + a per-request proof) instead of a Bearer token ([RFC 9449](https://www.rfc-editor.org/rfc/rfc9449)). Pass the **same key** the access token was bound to at sign-in:

```python
from jwcrypto import jwk

dpop_key = jwk.JWK.generate(kty="EC", crv="P-256") # the key the token was bound to

methods = await my_account.list_authentication_methods(
access_token=access_token,
dpop_key=dpop_key,
)
```

> [!WARNING]
> The `dpop_key` private key is a **Tier 0 secret**. Keep it in your secret store (KMS/HSM), never log it (`repr()` is redacted, but `key.export_private()` is not), use **one key per user/session** (never share across principals), and use **EC P-256 only** β€” any other key type fails closed with a `ValueError`.

## Error Handling

All errors inherit from `Auth0Error`. My Account API errors are `MyAccountApiError` (RFC 7807 problem-details, carrying `status`, `detail`, and optional `validation_errors`); missing arguments raise `MissingRequiredArgumentError`; transport or non-JSON responses surface as `ApiError`.

### Basic handling (recommended)

```python
from auth0_server_python.error import Auth0Error

try:
methods = await my_account.list_authentication_methods(access_token=access_token)
except Auth0Error as e:
return {"error": str(e)}
```

### Advanced handling (when actions differ by case)

```python
from auth0_server_python.error import Auth0Error, MyAccountApiError

try:
await my_account.enroll_authentication_method(
access_token=access_token,
request=EnrollAuthenticationMethodRequest(type="passkey"),
)
except MyAccountApiError as e:
if e.status == 401:
return redirect_to_login() # token expired
if e.status == 403:
return {"error": "Missing required scope"} # e.g. create:me:authentication-methods
if e.status == 400 and e.validation_errors:
return {"error": "Validation failed", "details": e.validation_errors}
raise
except Auth0Error as e:
return {"error": str(e)}
```

> [!NOTE]
> Enrollment raises `MyAccountApiError`/`ApiError`, whereas passkey **sign-in** (`ServerClient`) raises `PasskeyError`. They are two distinct API surfaces β€” an auth grant versus a My Account resource β€” so write the `except` that matches the call you made.

### Common error types

- **`Auth0Error`** (base): catch for general handling
- **`MyAccountApiError`**: My Account API errors with `status`, `detail`, optional `validation_errors`
- **`MissingRequiredArgumentError`**: a required parameter (`access_token`, `authentication_method_id`, `request`) was not provided
- **`ApiError`**: transport failure or a non-JSON error body
Loading