You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -191,6 +191,10 @@ Sign users up or in with [WebAuthn](https://www.w3.org/TR/webauthn-2/) passkeys
191
191
192
192
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).
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).
> 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.
521
522
523
+
### Verify with DPoP (sender-constrained tokens)
524
+
525
+
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:
526
+
527
+
```python
528
+
verify_response =await server_client.mfa.verify(
529
+
{
530
+
"mfa_token": mfa_token,
531
+
"otp": "123456",
532
+
},
533
+
dpop_key=dpop_key, # the same EC P-256 key the login was bound to
534
+
)
535
+
536
+
assert verify_response.token_type =="DPoP"
537
+
```
538
+
539
+
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.
540
+
541
+
> [!WARNING]
542
+
> 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**.
543
+
522
544
## Session Persistence
523
545
524
546
By default, `verify()` returns tokens without persisting them to the session store. However, you can automatically persist tokens by setting `persist=True`.
Copy file name to clipboardExpand all lines: examples/MyAccountAuthenticationMethods.md
+20-1Lines changed: 20 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ The [My Account API](https://auth0.com/docs/manage-users/my-account-api) lets a
6
6
> 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.
7
7
8
8
> [!NOTE]
9
-
> To **sign in** with a passkey (rather than manage one), see [examples/Passkeys.md](Passkeys.md).
9
+
> 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.
10
10
11
11
## Table of Contents
12
12
@@ -18,6 +18,7 @@ The [My Account API](https://auth0.com/docs/manage-users/my-account-api) lets a
18
18
-[4. Get a single authentication method](#4-get-a-single-authentication-method)
19
19
-[5. Update (rename) an authentication method](#5-update-rename-an-authentication-method)
20
20
-[6. Delete an authentication method](#6-delete-an-authentication-method)
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:
181
+
182
+
```python
183
+
from jwcrypto import jwk
184
+
185
+
dpop_key = jwk.JWK.generate(kty="EC", crv="P-256") # the key the token was bound to
> 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`.
195
+
177
196
## Error Handling
178
197
179
198
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`.
-[Completing MFA on a passkey login (and where the session comes from)](#completing-mfa-on-a-passkey-login-and-where-the-session-comes-from)
18
19
-[Error Handling](#error-handling)
19
20
@@ -135,6 +136,31 @@ result = await server_client.signin_with_passkey(
135
136
> [!NOTE]
136
137
> The SDK is transparent to the signup-vs-login difference in the credential `response` — both flow through the same `PasskeyAuthResponse.response` dict. Send exactly the keys the browser produced.
137
138
139
+
## 3. DPoP-bound passkey tokens (optional)
140
+
141
+
Pass an optional `dpop_key` to bind the issued 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 **opt-in**: omit `dpop_key` and sign-in returns ordinary Bearer tokens with no behaviour change.
142
+
143
+
```python
144
+
from jwcrypto import jwk
145
+
146
+
dpop_key = jwk.JWK.generate(kty="EC", crv="P-256") # you generate and keep this key (Tier 0)
When `dpop_key` is supplied, the SDK attaches a token-endpoint proof so Auth0 issues a DPoP-bound token, transparently handles the server-nonce challenge, and **rejects a Bearer downgrade** — if the server returns an unbound token, `signin_with_passkey` raises `PasskeyError` rather than silently accepting a token bound to a key it never used.
157
+
158
+
> [!TIP]
159
+
> Reuse the **same**`dpop_key` for any subsequent My Account API calls made with the resulting token — the token is bound to that one key. See [MyAccountAuthenticationMethods.md → DPoP](MyAccountAuthenticationMethods.md#dpop).
160
+
161
+
> [!WARNING]
162
+
> 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` before any network call.
163
+
138
164
### Completing MFA on a passkey login (and where the session comes from)
139
165
140
166
When a passkey login needs a second factor, `signin_with_passkey` raises `MfaRequiredError`**before** it creates a session. You finish the login by challenging and verifying through `client.mfa`, then **store the returned tokens yourself** — on this path the SDK does not persist the session for you (`persist` defaults to `False`, and there is no existing session to update yet):
@@ -146,6 +172,7 @@ try:
146
172
result =await server_client.signin_with_passkey(
147
173
auth_session=auth_session,
148
174
authn_response=authn_response,
175
+
dpop_key=dpop_key, # optional; omit for Bearer tokens
> Re-supply the **same**`dpop_key` to `verify`. Omitting it when the login was DPoP-bound would downgrade the result to a Bearer token; `verify`**rejects** that mismatch with `MfaVerifyError` rather than silently dropping the sender constraint. DPoP is preserved end to end — `persist=False` affects only *who writes the session*, never the token binding.
208
+
177
209
> [!NOTE]
178
210
> Do **not** pass `persist=True` on this path. It updates an *existing* session, and a passkey-first login has none yet, so it raises `MfaVerifyError("No existing session found…")` — discarding the tokens `verify` just obtained. Use `persist=False` and store the returned tokens as shown above. (This is pre-existing MFA-client behavior, unrelated to passkeys.)
179
211
@@ -220,7 +252,7 @@ except Auth0Error as e:
220
252
### Common error codes (`PasskeyErrorCode`)
221
253
222
254
-`passkey_challenge_error` — the signup/login challenge request failed
223
-
-`passkey_token_error` — token exchange failed
255
+
-`passkey_token_error` — token exchange failed (also used for a rejected DPoP downgrade)
224
256
-`invalid_response` — Auth0 returned a response that could not be parsed
0 commit comments