Skip to content

Latest commit

 

History

History
377 lines (286 loc) · 20 KB

File metadata and controls

377 lines (286 loc) · 20 KB

PASSKEY.md

Passkey UX Guidance for AI Coding Agents

Agent integration

Reference or link this file before generating any passkey or WebAuthn implementation. Agents that skip it reproduce the same errors by default.

Add to your agent's knowledge system — for example, in Claude Code's CLAUDE.md:

@https://raw.githubusercontent.com/YubicoLabs/passkey-ux/main/PASSKEY.md

The same URL works in Cursor (.cursorrules), GitHub Copilot (.github/copilot-instructions.md), and any RAG or custom agent system that accepts URL or file references.

If you are an AI coding agent: read this entire file before writing any passkey code. Every anti-pattern below is a real failure mode that agent-generated code produces by default.


1. Philosophy

  • Give users choice. Don't restrict authenticator types unless mandated by policy or regulation.
  • Consider all user groups. Assurance levels, accessibility needs, platform diversity, risk profiles.
  • Allow individuals to eliminate passwords. Support modes that replace passwords with passkeys/security keys and remove phishable recovery options (e.g., OpenAI Advanced Account Security, Google Advanced Protection Program). See advanced-protection-program.md for design requirements, recovery patterns, and APP-specific anti-patterns.
  • Steer, don't restrict. Guide users toward their familiar authenticator without excluding alternatives.
  • The server is the security boundary. The server validates the credential in the response. Client-side filtering is a UX optimization, not a security control.

2. Onboarding / Enrollment

  • Timing: Post-login, not registration. 3-4x better conversion (FIDO Alliance).
  • Single prompt: No custom modal before navigator.credentials.create(). System sheets handle confirmation.
  • No nag loop: Once per device. Honor dismissal 30+ days. Track server-side.
  • Positive framing: "Sign in faster" not "less secure method."
  • Second passkey nudge: Prompt for a second on a different device after first enrolled.
  • residentKey: "required": Always create discoverable credentials. Enables conditional UI, hints steering, and discoverable assertion flows.
  • userVerification: "required" optimizes for security assurance: the authenticator must verify the user (biometric, PIN) before every ceremony. "preferred" optimizes for conversion: the authenticator attempts verification but succeeds without it, which may result in a single-factor authentication. If a relying party sets "preferred", it must check the UV flag in the authenticator response and perform step-up authentication (e.g., password, second factor) when UV is not set. Regardless of the setting, relying parties must always check the UV flag in the response.

Reference: AP-04, AP-07, AP-08, AP-10, AP-11


3. Authentication

  • Conditional UI + modal button. Always ship both. Conditional UI is not universal.
  • Non-passkey fallback. "Sign in another way" (password, magic link). Exception: passwordless users fall back to cross-device auth, additional passkeys, security keys, or identity proofing.
  • Adaptive hints: Server records authenticator type on successful sign-in, then sends matching hints with empty allowCredentials on the next sign-in to steer the browser picker.
  • Privacy: Identical error responses regardless of account/credential state. W3C spec requirement.

Reference: AP-01, AP-03, AP-05, AP-06, AP-12


4. Cross-Device Bootstrapping

  • Cross-device auth: QR + Bluetooth (caBLE). Platform passkeys and security keys both support cross-device authentication. Don't suppress options.
  • Fallback then enroll: Sign in via fallback on new device, then enroll a passkey. Passwordless users: cross-device auth, security key, or identity proofing.
  • Empty allowCredentials for discovery: Enables cross-device discoverable credential lookup.

Reference: AP-02


5. Passkey Management

  • Credential list: Name (user-editable), created date, last used, authenticator type.
  • FIDO MDS: Use the FIDO Metadata Service to show authenticator details (manufacturer, model, icon).
  • Removal: Allow removing individual passkeys. Warn on last one. Require re-auth or alternative setup before removing the last credential.
  • Multi-passkey nudge: Visual cue when user has one passkey. Show credential count.

6. Recovery

  • Not "forgot your passkey": Passkeys are not secrets. Losing a passkey means losing device access. The remedy is account recovery, not passkey reset. Never add a passkey-specific recovery flow.
  • Platform sync as implicit recovery: Synced passkeys mean "try on your other device" before account recovery.
  • Cross-device auth: Authenticate from another device to unlock a session.
  • Account recovery fallback: Standard recovery options (email, magic link, recovery codes) are phishable. User choice.
  • Advanced security mode: Passwordless users disable phishable recovery. Recovery is additional passkeys, security keys, or identity proofing only. Opt-in with clear communication that support cannot assist. See advanced-protection-program.md for full requirements and anti-patterns.
  • Identity proofing: Government ID verification, video verification, or in-person verification as recovery and new-device bootstrap. Configure before going passwordless.
  • Credential portability: FIDO CXP (Cross-Device Credential Exchange) addresses cross-ecosystem migration.
  • Security keys don't sync. Lost key = lost credentials. Register a second on separate hardware.

Reference: AP-02, AP-12


7. Enterprise Considerations

  • Policy-mandated authenticator types: Enforce via authenticatorAttachment only when policy requires specific authenticator types. Use "cross-platform" when policy or regulation mandates roaming authenticators (e.g., hardware security keys for phishing-resistant MFA, shared workstations where credentials must travel with the user). Inform decisions with FIDO MDS and government certifications (e.g., FIPS).

8. Anti-Pattern Reference

ID Anti-Pattern Most Common Mistake
AP-01 No fallback path Passkey-only auth with no escape hatch
AP-02 Passkey-only on new device No path for users with no registered passkeys
AP-03 Revealing credential existence Leaking whether an account exists
AP-04 Enrollment during registration Prompting before trust is established
AP-05 Conditional UI without modal fallback Invisible failure on browsers without conditional UI
AP-06 Platform-lock authenticatorAttachment: "platform" silently excludes security keys and hybrid transport; hints ignored with populated allowCredentials
AP-07 Upgrade prompt on every visit Showing "add a passkey" after every login trains users to dismiss
AP-08 Single passkey One passkey is a single point of failure
AP-09 allowCredentials: [] in modal flow Non-discoverable credentials silently fail; but empty array is correct for discoverable + hints flow
AP-10 Guilt messaging "You're using a less secure sign-in" creates anxiety, not adoption
AP-11 Double-prompting Custom modal before navigator.credentials.create() causes two dialogs
AP-12 "Forgot your passkey?" Importing password-recovery mental model into passkey UX

AP-01

DECISION: Always provide at least one non-passkey authentication path
CORRECT:   Passkey sign-in with visible "other options" (password, magic link, SMS)
INCORRECT: Passkey-only login page with no visible alternative
RATIONALE: Users arrive at your login page from new devices, incognito sessions,
           work computers, and guest browsers — none of which have their passkey.
           A passkey-only page locks them out. Even users who enrolled a passkey
           may need a fallback for their first login on a new device before they
           register a second passkey there.

           Exception: users enrolled in passwordless-only mode (e.g., advanced
           account security). For these users, fallback is cross-device auth,
           additional passkeys, security keys, or identity proofing.

AP-02

DECISION: Always provide a cross-device or fallback path for first login on a new device
CORRECT:   "Sign in with a passkey" → if no passkey found → "Use another device" / "Use password"
INCORRECT: Passkey-only page on a new device where the user's passkey isn't available
RATIONALE: A user with passkeys registered on their phone who opens your site on a
           new laptop has no passkey on that laptop yet. If your site is passkey-only,
           they are locked out. The bootstrapping path is: (1) cross-device auth
           (use another device's passkey via QR code), or (2) fallback to password/magic link,
           then enroll a passkey on the new device post-login. Platform passkeys and
           security keys both support cross-device authentication.

AP-03

DECISION: Return identical error responses whether an account exists or not
CORRECT:   "Passkey authentication failed" (same message always)
INCORRECT: "No passkey found for this email" or "Account not found"
RATIONALE: W3C WebAuthn Level 2 §14.6.2 (Username Enumeration) requires that
           relying parties not reveal whether a credential exists for a given
           user handle. An attacker can enumerate registered accounts by testing
           emails and observing error message differences. "No passkey found for
           this email" confirms the account exists. Use a generic error always.

SECURITY NOTE: This anti-pattern is a W3C spec violation. Treat it as a security
defect, not a UX preference. Apply to all auth-related error responses (login,
registration, recovery). Use constant-time credential lookups. Never differentiate
error cases in any user-visible or network-observable way.

Reference: W3C WebAuthn Level 2 §14.6.2, OWASP Authentication Cheat Sheet


AP-04

DECISION: Prompt for passkey enrollment after first successful login, not at signup
CORRECT:   User creates account (email + password) → logs in → sees enrollment prompt
INCORRECT: User creates account → immediately asked to "set up your passkey now"
RATIONALE: FIDO Alliance research shows contextual post-login enrollment converts
           3-4x better than registration-flow prompts. At signup, users don't yet
           trust the service and don't understand why they'd add a passkey. After
           login, they're authenticated, oriented, and the security benefit is
           immediately tangible: "Next time, skip the password."

AP-05

DECISION: Implement mediation:conditional AND an explicit "Sign in with a passkey" button
CORRECT:   navigator.credentials.get({mediation: "conditional", ...}) + visible button
INCORRECT: mediation:conditional only, assuming all browsers support it
RATIONALE: Not all browsers support conditional UI (mediation:conditional). On
           unsupported browsers, a conditional-only implementation produces no
           sign-in prompt — the user sees an empty autofill field and no way to
           trigger passkey auth. The modal button is the fallback for these browsers
           AND for users who don't notice the autofill indicator.

AP-06

DECISION: Never set authenticatorAttachment: "platform" for general-purpose enrollment
CORRECT:   Omit authenticatorAttachment entirely. To steer without restricting,
           use hints: ["client-device"] (WebAuthn Level 3).
INCORRECT: authenticatorSelection: { authenticatorAttachment: "platform" }
RATIONALE: "platform" does not prefer platform passkeys — it hard-excludes everything
           else. Security keys (USB, NFC, BLE) are silently removed from the picker.
           Cross-device auth via phone QR + Bluetooth is also excluded.
           "platform" does not mean "synced" — some platform credentials are
           device-bound and don't sync. The flag filters by authenticator type,
           not sync behavior. Agents and tutorials default to it; no one questions it.

NOTE: One defensible use: password-upgrade flows where a signed-in user replaces
      their password with a platform passkey. Do not apply to general enrollment.

HINTS + allowCredentials: browsers may ignore hints when allowCredentials lists
credentials with internal transport. Send empty allowCredentials to make hints
effective (requires residentKey: "required" at registration).

Reference: W3C WebAuthn Level 3 §5.4.4 (AuthenticatorSelectionCriteria)


AP-07

DECISION: Show the passkey enrollment prompt once per user per device; honor dismissal
CORRECT:   Show once after login → if dismissed, suppress for ≥30 days → resurface once
INCORRECT: Show "Add a passkey for faster sign-in!" banner on every login
RATIONALE: Users who dismiss the enrollment prompt have made a decision. Repeating the
           prompt on every visit trains them to dismiss it reflexively — and creates a
           negative association with passkeys. FIDO Alliance research shows a single
           well-timed prompt converts better than repeated prompts. Honor the dismissal
           with a minimum 30-day suppression; one resurface allowed.

IMPLEMENTATION NOTE: Track dismissal in user preferences (server-side), not localStorage.
localStorage is cleared in incognito and when users clear browser data — server-side
suppression is durable.

AP-08

DECISION: Prompt users to register a second passkey on a second device after enrollment
CORRECT:   After first passkey enrolled: "Add a passkey on another device"
INCORRECT: One passkey registered → done, no further prompting
RATIONALE: A passkey on a single device is a single point of failure. If the user
           loses that device, breaks it, or switches platforms, they lose passkey access.
           Security key users have the same problem — if the key is lost, passkey access
           is gone. The correct pattern: prompt for a second passkey (different device or
           second security key) at enrollment completion.
NOTE:      Don't call the second credential a "backup." That frames one passkey type as
           subordinate. A security key and a platform passkey are peers with different
           properties. Use neutral language: "Add another passkey."

IMPLEMENTATION NOTE: Trigger the second-passkey prompt immediately after first enrollment
completes — not at the next login. The user is in setup mode and most receptive.
Show credential count ("You have 1 passkey — add another on a different device?").

MUTUAL EXCLUSION: The enrollment banner and second-passkey nudge are mutually exclusive.
Check passkey count on every dashboard entry: count == 0 → enrollment banner (if not
dismissed); count == 1 → second-passkey nudge (if not dismissed); count ≥ 2 → both
hidden. Never display both simultaneously. Reset nudge state on sign-out to prevent
stale DOM state from carrying across sessions.

AP-09

DECISION: Never copy allowCredentials: [] from a conditional UI example into a modal flow
CORRECT:   Modal flow: send the user's registered credential IDs in allowCredentials
INCORRECT: Modal flow: allowCredentials: []
RATIONALE: allowCredentials: [] tells the browser to search for any passkey for this site.
           It works in conditional UI (mediation: "conditional"). In a modal flow for a
           known user, it silently skips credentials the browser can't discover on its own.
           Always send credential IDs when you know who the user is.
           Exception: adaptive hints flow — see AP-06 for when empty is correct here too.

Reference: W3C WebAuthn Level 3 §5.5 (PublicKeyCredentialRequestOptions), CTAP 2.1 §6.2


AP-10

DECISION: Never show negative framing about non-passkey authentication methods
CORRECT:   "Add a passkey to sign in faster next time" (positive, optional)
INCORRECT: "You're using a less secure sign-in method. Upgrade to a passkey."
RATIONALE: Guilt messaging about existing credentials creates anxiety without action.
           Users who can't add a passkey (wrong device, wrong browser, IT policy) are
           told they're doing something wrong with no path to fix it. Users who could
           add a passkey are more likely to be annoyed than motivated. Positive framing
           ("faster, no password needed") outperforms negative framing.

AP-11

DECISION: Never show a custom modal before calling navigator.credentials.create()
CORRECT:   Show a brief inline explanation, then call create() — system sheet appears
INCORRECT: Show your own "Add a passkey?" modal → user taps OK → system sheet appears
RATIONALE: Mobile platforms show a native system sheet when navigator.credentials.create()
           is called. If you show your own modal first, the user gets two prompts: yours
           and the system's. This doubles friction, confuses users ("I already said yes"),
           and suggests to the user that your app is poorly built.

AP-12

DECISION: Never add a "forgot passkey" recovery flow
CORRECT:   Offer "Sign in a different way" (fallback to existing credential)
INCORRECT: "Forgot your passkey? Reset it via email"
RATIONALE: Passkeys are device-bound credentials, not secrets the user memorizes.
           "Forgetting" a passkey means losing access to the device — the correct
           remedy is using another registered device or account recovery, not a
           passkey-specific reset flow. Adding one trains users to think of passkeys
           like passwords — eroding the UX and security model.

9. How AI Agents Should Use This Document

  1. Read the Philosophy section first — it governs every decision.
  2. Read the lifecycle section relevant to your task (Onboarding, Authentication, etc.).
  3. Read the full anti-pattern entry for each referenced AP.
  4. If implementing conditional UI: AP-05 is mandatory reading.
  5. If implementing enrollment: AP-04 and AP-11 are mandatory reading.
  6. If implementing error handling: AP-03 is a security requirement, not a preference.
  7. If implementing hints-based steering: AP-06 and AP-09 are both mandatory reading.

Do not add "forgot passkey" flows. Do not prompt during signup. Do not ship conditional-UI-only. Do not set authenticatorAttachment: "platform" for general enrollment. Do not use allowCredentials: [] in modal flow unless all credentials are discoverable and you are using hints to steer the browser (see AP-06, AP-09).

For behavioral requirements, server API contract, and client state machine, see examples/.

For high-assurance authentication programs (Advanced Protection Program, phishing-resistant-only sign-in, constrained recovery), see advanced-protection-program.md.


Further reading

  • FIDO Alliance Design Guidelines — UX patterns for enrollment, authentication, and recovery
  • W3C WebAuthn Level 3 — authoritative specification
  • MDN Web Authentication API — API reference and browser compatibility
  • developers.yubico.com — WebAuthn and FIDO2 developer documentation from Yubico
  • docs.yubico.com — YubiKey integration guides and technical references
  • passkeys.dev — developer guides, platform compatibility, and implementation patterns
  • Google Identity — Passkeys — Android, Chrome, and Google Password Manager passkey integration
  • web.dev/passkeys — implementation guides and code examples from the Chrome team
  • Apple Developer — Supporting Passkeys — iOS/macOS implementation guidance

Contributing

Found an anti-pattern PASSKEY.md missed? File an issue:

Platform: [iOS / Android / Chrome / Firefox / Safari / Security key / other]
Anti-pattern: [name or AP-XX]
What agent generated: [code snippet]
What went wrong: [user-visible failure]