Skip to content

Passkey login fails with webauthn_verification_failed ("Backup Eligible flag inconsistency") for Apple iCloud synced passkeys on subsequent logins #2732

Description

@drandreumartinez
  • I confirm this is a bug with Supabase, not with my own application.
  • I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

POST /passkeys/authentication/verify rejects a valid assertion when the authenticator's Backup Eligible (BE) flag differs between registration and a later authentication. The client only receives the generic webauthn_verification_failed; the server log reveals the real cause: "Backup Eligible flag inconsistency detected during login validation".

This breaks passkey login for Apple iCloud Keychain passkeys, which report inconsistent BE/backup flags before vs. after iCloud sync — producing a "first login works, later login fails" pattern. Observed in production on Apple devices (Safari/Brave, iCloud Keychain) and reproduced deterministically in a local Supabase CLI stack.

Exact server log of the failing request:

{"component":"api","error":"Backup Eligible flag inconsistency detected during login validation",
 "error_code":"webauthn_verification_failed","level":"warning","method":"POST",
 "path":"/passkeys/authentication/verify","status":400}

The client only sees {"code":"webauthn_verification_failed","message":"Credential verification failed"} (the real reason is in WithInternalError, not exposed).

Root cause: go-webauthn enforces BE-flag immutability during FinishLogin. Per the WebAuthn/CTAP behavior of synced credentials (Apple iCloud Keychain), the BE/backup flags can legitimately differ between ceremonies, so strict rejection is inappropriate for these credentials.

To Reproduce

Deterministic reproduction with Chrome DevTools virtual authenticator:

  1. Enable a virtual authenticator (WebAuthn.addVirtualAuthenticator, ctap2, internal, resident, UV).
  2. Register a passkey normally → stored credential has backup_eligible=false.
  3. Before the next assertion, re-add the same credential (same credentialId/privateKey/userHandle) via WebAuthn.addCredential with backupEligibility: true (simulating iCloud flipping the flag after sync).
  4. Call signInWithPasskey()verify returns 400 with the log line above.

Control cases confirming scope (both succeed): BE=false stable across registration + multiple logins, and BE=true stable across registration + multiple logins. Only the BE flip fails.

Expected behavior

Relax/handle the BE-flag inconsistency for backup-eligible (synced) credentials rather than failing the login — matching the more lenient posture of other libraries (e.g. @simplewebauthn/server returns the backup flags for the RP to decide and does not reject). At minimum, surface the specific reason to the client instead of the generic webauthn_verification_failed, so RPs can diagnose without server logs.

Screenshots

N/A (server log included above).

System information

  • OS: macOS (also observed from iOS/Safari)
  • Browser: Safari, Brave, Chrome (reproduction uses Chrome DevTools virtual authenticator)
  • Version of supabase-js: 2.112.3 (pinned; auth.experimental.passkey: true)
  • GoTrue / supabase/auth: v2.195.0 (passkeys beta, GOTRUE_PASSKEY_ENABLED=true)

Additional context

The strict check lives in go-webauthn's FinishLogin; GoTrue v2.195.0 has no env/config to relax it. Reference: passkey_authentication.go @ v2.195.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions