Hi @arpita.vyas23
I understand that you are having issues regarding the best practice of user re-authentication/step-up authentication using passkeys inside your SPA ( Singe Page App) application.
For a modern, secure, and phishing-resistant architecture, you should strictly avoid handling raw WebAuthn challenges, assertions (navigator.credentials.get), and cryptographic validations manually in your frontend or backend. Handling these APIs manually is prone to implementation vulnerabilities, especially regarding origin binding, replay attacks, and token validation.
Auth0 provides an out-of-the-box, OIDC-native Step-Up Authentication flow that manages the passkey WebAuthn ceremony securely on its hosted pages and issues a cryptographically signed cryptographic token (Access/ID Token) containing the proof of re-authentication. The documentation about Adding Step Up Authentication is available on our website.
[RECOMMENDED APPROACH]
Instead of manual WebAuthn calls , you should use the standard OIDC Step-Up Re-authentication pattern using the standard parameters max_age=0 (to force a fresh challenge) and/or acr_values (to require a specific authentication strength, like MFA or Passkeys).
Why this is preferred:
-
No Raw Key Handling: Your SPA and backend never handle raw WebAuthn assertions, removing the risk of client-side tampering .
-
No Cryptographic Math on Backend: Auth0 does the heavy cryptographic validation. Your backend simply validates a standard Auth0-signed JWT.
-
Phishing Resistance: Auth0 handles the Relying Party ID (RP ID) binding securely.
When a user in your SPA clicks “Change Email”, your SPA must request a fresh, highly-assured Access Token.
Step A: Initiate Re-authentication from the SPA
- Call your Auth0 SPA SDK’s login method (e.g.,
loginWithRedirect or getTokenSilently with popup fallback), passing parameters to force fresh, high-assurance authentication
// SPA code when user clicks "Change Email"
auth0Client.loginWithRedirect({
authorizationParams: {
max_age: 0,
acr_values: 'http://schemas.openid.net/pape/policies/2007/06/multi-factor',
scope: 'openid email update:current_user_email',
redirect_uri: 'https://myapp.com/settings/callback'
}
});
-
Application (Client) ID: Use your existing SPA Client ID. You do not need to create a new client or configure a separate app.
-
Extra Config Required: In the Auth0 Dashboard, navigate to your Custom API settings (the audience your SPA is calling) and ensure that the scope update:current_user_email is defined. Also, ensure you have enabled Passkeys (FIDO2 WebAuthn) as an authentication factor under Security > Multi-Factor Auth.
Once the authentication completes, the SPA receives a fresh Access Token. The SPA sends this Access Token in the Authorization: Bearer <TOKEN> header of the “Change Email” request to your Backend API .
Verification steps your Backend API must perform:
-
Standard JWT Validation: Verify the token’s signature, issuer (iss), and audience (aud) using your standard Auth0 public keys.
-
Check the auth_time Claim: Ensure the token contains an auth_time claim. Calculate current_time - auth_time. Since you passed max_age: 0, the auth_time must be less than 60–120 seconds ago. This proves the passkey was tapped just-in-time for this transaction, not hours ago.
-
Verify amr (Authentication Methods Reference): Look at the amr array in the token . It must contain mfa and webauthn (or fido), proving a biometrics/passkey authentication took place, not a standard password.
-
Scope Check: Ensure the token carries the update:current_user_email scope.
If all four conditions pass, your Backend API is cryptographically assured that the current user physically tapped their passkey seconds ago. It can now safely call the Auth0 Management API to update the user’s email.
Industry Best Practices for Passkey Re-authentication
-
Graceful Fallbacks (The “MFA Coop”): If a user originally signed up with a password and has not enrolled a passkey yet, the step-up flow should gracefully route them to enroll a Passkey first, or fall back to their registered MFA device (e.g., authenticator app/SMS).
-
Never handle WebAuthn state on the Backend: Do not attempt to store WebAuthn challenges in your Postgres/Redis database or write custom cryptographic verification scripts. Offloading this to Auth0 guarantees compliance with WebAuthn L2/L3 security specs.
-
Avoid Silent Tokens for Sensitive Routes: Ensure your API route ignores any Access Tokens retrieved via silent token rotation (getTokenSilently) if they do not pass the auth_time freshness test (e.g., must be less than 5 minutes old). Always force an interactive redirect for sensitive operations.
Kind Regards,
Nik