WebAuthn Platform Enrollment Skip Behavior

snoozeEnrollment() and refuseEnrollmentOnThisDevice() fail to complete transaction when enrollment is triggered via api.authentication.enrollWith() in a Post-Login Action

Category: Multi-factor Authentication / WebAuthn / Actions

**Summary:**When WebAuthn platform enrollment is triggered programmatically via api.authentication.enrollWith({ type: 'webauthn-platform' }) in a Post-Login Action, neither snoozeEnrollment() nor refuseEnrollmentOnThisDevice() from the ACUL SDK are able to complete the login transaction when called from the Universal Login enrollment screen. Both methods result in the transaction failing and the user being unable to log in.

This appears to be because enrollWith creates a transaction context where Auth0 expects enrollment to be completed or explicitly refused at the policy level — but neither SDK method satisfies that requirement when enrollment is action-driven rather than policy-driven (progressive enrollment).

The only known workaround is calling reportBrowserError() with a synthetic error on skip, which signals a browser-side WebAuthn failure and allows Auth0 to continue the transaction without enrollment being completed. This suggests snoozeEnrollment() and refuseEnrollmentOnThisDevice() are only designed to work within Auth0’s native progressive enrollment flow, not action-triggered enrollment, and the documentation does not make this distinction clear.Ready to post? :magnifying_glass_tilted_right: First try searching for your answer.

Hi again @dhrupal.patel

I can see that you are experiencing similar issues posted in your previous topic regarding the skip behavior of the WebAuth enrollment. In order to address the more specific issues you have posted here:

You are reporting that when WebAuthn platform authenticator enrollment is triggered programmatically via api.authentication.enrollWith({ type: 'webauthn-platform' }) in a Post-Login Action, neither snoozeEnrollment() nor refuseEnrollmentOnThisDevice() from the Advanced Customizations for Universal Login (ACUL) SDK can complete the login transaction. Both methods result in transaction failure and the user being unable to log in, and the only workaround is calling reportBrowserError() with a synthetic WebAuthn error to signal a browser-side failure.

Root Cause: Auth0 does not officially support optional MFA enrollment with skip or snooze functionality when enrollment is triggered via api.authentication.enrollWith() in a Post-Login Action. When enrollWith() is called, it creates a mandatory enrollment transaction that Auth0 expects to be completed before the authentication flow can proceed. The ACUL SDK methods snoozeEnrollment() and refuseEnrollmentOnThisDevice() are designed to work only within Auth0's native progressive enrollment flow (where enrollment is policy-driven), not with action-triggered enrollment. These methods do not satisfy the transaction completion requirement imposed by enrollWith(), leaving the transaction in a failed state.

The reportBrowserError() workaround succeeds because it signals a WebAuthn API failure at the browser level, which Auth0 interprets as a legitimate technical reason to bypass the enrollment requirement and allow the transaction to complete. This is not an intended use of the method and should not be relied upon as a permanent solution.

Official Limitation: According to Auth0 support documentation, skipping MFA enrollment is not supported when MFA is enabled. The platform requires that when an enrollment is triggered, it must be completed before authentication can proceed. There is no tenant-side configuration, Login Flow Action, or MFA enrollment policy setting that can change this behavior when using enrollWith().

Recommended Alternatives:

  1. Use enrollWithAny() instead of enrollWith() — This method allows users to skip enrollment if they already have at least one other MFA factor enrolled. If the user has no other factors, they are still required to enroll in one of the available options, but they can choose which factor to use.

    Example:

    exports.onExecutePostLogin = async (event, api) => {
      if (event.user.enrolledFactors.length === 0) {
        api.authentication.enrollWithAny({ 
          factors: ['webauthn-platform', 'otp'] 
        });
      }
    };
  2. Use conditional logic to avoid triggering enrollment for specific users — Instead of calling enrollWith() for all users, check user metadata, group membership, or other conditions to determine whether enrollment should be prompted at all.

    Example:

    exports.onExecutePostLogin = async (event, api) => {
      if (event.user.app_metadata.requiresWebAuthnEnrollment) {
        api.authentication.enrollWith({ 
          type: 'webauthn-platform' 
        });
      }
    };
  3. Prompt users for enrollment outside the authentication flow — Implement enrollment as an optional, post-login user preference in your application rather than as a mandatory step during authentication. This allows users to enroll at their own pace without blocking login.
  4. Use tryAnotherMethod() from the ACUL SDK — If you are using a custom enrollment screen, the tryAnotherMethod() method allows users to switch to a different MFA factor, but this does not allow them to skip enrollment entirely.

What the documentation does not clarify: The ACUL SDK documentation for snoozeEnrollment() and refuseEnrollmentOnThisDevice() does not explicitly state that these methods only work with policy-driven progressive enrollment and will fail when enrollment is triggered via api.authentication.enrollWith() in a Post-Login Action. This distinction should be documented to prevent developers from attempting to use these methods in action-triggered scenarios where they are not supported.

Submitting feedback: If optional enrollment with skip/snooze functionality is required for your use case, Auth0 recommends submitting a feature request using the Auth0 Product Feedback page.

You might want to dig into these resources:

We hope this clarifies the limitation and provides a path forward for your implementation.

Kind Regards,
Nik

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