Biometrics Skip / MFA Completion — Investigation Synopsis
Background
The member portal has a biometrics enrollment screen shown during the Auth0 Universal Login (UL) flow. This screen appears after email/phone MFA is satisfied and prompts the user to enroll in WebAuthn (biometrics) or skip. The requirement is that this screen always appears inside the UL flow so the user can enroll or consciously skip.
The Core Problem
When a user clicks “Skip” on the biometrics enrollment screen, the app calls snoozeEnrollment() on the Auth0/UL side. This call completes successfully, but Auth0 then redirects back to the member portal with an error in the URL hash instead of an access token. The member portal’s handleLoginResponse in service.ts calls auth0Client.parseHash(), receives a non-null err, and treats it as a login failure — showing a login error screen instead of completing the login.
The user’s email/phone MFA was already satisfied before the biometrics screen appeared, so this is incorrect behavior — the login should complete successfully.
What We Investigated (Dev)
1. Where the error originates
Added logging in handleLoginResponse to capture the raw parseHash error. The console showed:
[parseHash error] { error: 'server_error', errorDescription: undefined }
The full error_description was URL-encoded in the raw hash: MFA was%20... — Auth0-js was not surfacing it on the err object, so we parsed it directly from window.location.hash.
2. Why server_error is returned
The Auth0 Action (mfaEnablement) contains this block:
if ((isUniversalLoginFlow || isMobileUniversalLoginFlow) && !isEnrolledInBiometrics && isWebAuthnAvailable) {
api.authentication.enrollWith({ type: 'webauthn-platform' });
}
enrollWith makes biometric enrollment a required step in the Auth0 authorization flow. When snoozeEnrollment() is called to skip it, Auth0 considers a required step incomplete and cannot issue a token — so it redirects back with server_error instead.
3. Client-side fallback via checkSession
Added a guard in handleLoginResponse to detect the WebAuthn skip error and fall back to checkSession to silently retrieve a valid session token:
if (isMfaWebAuthnSkip) {
auth0Client.checkSession({ prompt: 'none' }, (sessionErr, sessionResponse) => { ... });
}
Why it failed: checkSession also failed. Because Auth0 returned server_error, it never established a session — there was nothing to retrieve. The client-side app cannot recover from a flow that Auth0 itself considers incomplete. The fix must be in the Auth0 Action.
What We Tried in Prod
Attempt 1: Disable biometrics via Auth0 dashboard (Security > Multi-Factor Auth)
Turning off WebAuthn in the Auth0 dashboard to see if removing it at the platform level would allow the flow to complete cleanly.
What happened: This caused a security issue screen to appear for users, breaking login entirely. Turned back on immediately.
Attempt 2: Comment out the enrollWith line in the prod action
With biometrics re-enabled in the dashboard, commented out just the enrollWith call in the action to see if the skip error would go away while keeping MFA intact.
What happened: The biometrics prompt disappeared as expected — but email/phone MFA also stopped being challenged. Users bypassed 2FA entirely. This suggests the presence of enrollWith in the flow is affecting how Auth0 sequences the MFA challenge + enrollment steps, not just controlling the enrollment screen itself.
Why We Are Blocked
The fundamental conflict is:
enrollWith makes biometrics a mandatory step in Auth0’s flow, which means skipping it causes server_error — unrecoverable client-side
- Removing it breaks the requirement that the screen appears in UL, and demonstrably breaks MFA challenge sequencing
- An alternative approach using
api.redirect.sendUserTo() was considered — this would show the enrollment screen as a redirect step and resume the Auth0 flow via /continue whether the user enrolled or skipped. However, this would require refactoring multiple applications and may not ultimately solve the problem, making it not a viable path forward at this time
- No other Auth0-supported mechanism has been identified that allows an optional enrollment step inside the UL flow where skipping still results in a token being issued
Current State
- Universal Login already has
snoozeEnrollment wired up as intended
- Member portal changes (the
checkSession fallback and associated logging) are sitting in a feature branch and have not been deployed — they are confirmed not to work and should be cleaned up before the branch is merged
- The prod Auth0 action is unchanged and restored to its original state
- The feature is blocked pending a viable solution for handling the skip case within the Auth0 action