-
Notifications
You must be signed in to change notification settings - Fork 3
feat: passkeys, myaccount with dpop support #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rmad17
wants to merge
26
commits into
main
Choose a base branch
from
SDK-8780-passkey-feature
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
611e8c9
Changes for passkey implementation
rmad17 ec66c0f
Added missing test cases and edge case fix
rmad17 d2d1f21
Resolved snake case to camel case for correct parsing
rmad17 7691a0c
Reverting to snake case - as per auth0 api docs.
rmad17 3233fff
Reverted lint fixes for easier review
rmad17 66071e9
Edge case fix for Double URL-encoding and extra validation check
rmad17 3809edb
SDK-8780 PR review changes
rmad17 d299dbf
SDK-8780 Added review changes for integrating passkey sign-in with SD…
rmad17 0aacc8e
PR Review Changes
rmad17 786b4ec
MFA Support for Passkeys
rmad17 7e9225f
Sync with main
rmad17 22d2997
Changes for MyAccount Factors Schema
rmad17 a0100b7
Resolved merge conflicts
rmad17 69ea847
Added docs and feedback changes
rmad17 f4308ab
Updated example docs
rmad17 e91b27e
Updated Examples
rmad17 6dc142a
Linting fix for imports
rmad17 4fe53a4
Addressed Review comments for passkeys
rmad17 ec91808
Resolved conflicts
rmad17 922a3d2
Resolved review feedback
rmad17 a4e8757
Updated documentation, test cases and parameter type for dpop nonce
rmad17 74dbde9
README Changes and MFA refactor revert
rmad17 f8b76c4
MFA test fixes post revert
rmad17 cb8a8f9
Doc changes and minor code optimizations
rmad17 2f18b69
Added missing docstrings
rmad17 6d7a951
Removed duplicate Dpop docs
rmad17 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
| 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="passkey|abc123", | ||
| request=UpdateAuthenticationMethodRequest(name="My Work Laptop"), | ||
| ) | ||
| ``` | ||
|
|
||
| ## 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 | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.