Choosing authenticatorAttachment: Platform vs Cross-Platform

authenticatorAttachment is a single optional string, but setting it wrong silently excludes half your users’ authenticators from a ceremony. This page pins down exactly what each value does at registration and authentication, when a pin is justified, and how it composes with residentKey and userVerification. It is a focused decision at the level of one option; for the full comparison, see the parent platform vs roaming authenticator trade-offs.


Option Behaviour Reference

authenticatorAttachment At registration (create) At authentication (get) When to use
unset Any authenticator may register Any may respond Default; broadest coverage
'platform' Only the built-in authenticator Only built-in Mobile-first, low-friction, synced passkeys
'cross-platform' Only removable keys / hybrid Only removable / hybrid High-assurance, device-bound workforce

Note: at authentication, authenticatorAttachment is not a top-level option of PublicKeyCredentialRequestOptions; you steer roaming vs platform at get() time mainly through allowCredentials[].transports and the credential set, not an attachment field. The attachment pin is primarily a registration control.


Root Cause Analysis

1. Pinning 'platform' on a device with none. A desktop without Windows Hello/Touch ID throws ConstraintError (or NotAllowedError) at create(), and the user cannot enrol at all.

2. Pinning 'cross-platform' and blocking mobile passkeys. Forcing a security key excludes the frictionless platform-passkey path most consumers expect.

3. Assuming attachment controls sync. Sync is a credential-manager property; attachment only chooses the authenticator kind. Verify with credentialDeviceType/credentialBackedUp server-side, as in the trade-offs cluster.


Step-by-Step Resolution

Step 1 — Default to unset

// generateRegistrationOptions
authenticatorSelection: {
  // authenticatorAttachment omitted → both platform and roaming allowed
  residentKey: 'preferred',
  userVerification: 'preferred',
},

Step 2 — Pin only for a policy reason

// High-assurance workforce: require a removable security key
authenticatorSelection: {
  authenticatorAttachment: 'cross-platform',
  residentKey: 'required',
  userVerification: 'required',
},

Step 3 — Record the attachment that actually answered

const attachment = credential.response.getAuthenticatorAttachment?.(); // 'platform' | 'cross-platform' | null
await saveCredential({ credentialId, attachment, aaguid });

Step 4 — Offer the other kind on failure

Catch ConstraintError/NotAllowedError at create() and re-prompt with the pin removed so the user can enrol whatever they have.


Verification and Testing

Use the DevTools WebAuthn virtual authenticator:

1. Set attachment 'platform', virtual authenticator "internal"       ⇒ create() succeeds
2. Set attachment 'platform', virtual authenticator "usb" (roaming)  ⇒ ConstraintError/NotAllowedError
3. Unset attachment, either authenticator                            ⇒ create() succeeds

Assert that your server stores the reported attachment and that a ConstraintError re-prompts with attachment unset. Confirm credentialDeviceType is persisted so sync assumptions are data-driven.


Pitfalls

1. Hard-pinning 'platform'. Locks out desktop-without-biometric users. Leave unset or fall back.

2. Confusing attachment with sync. Read credentialBackedUp; do not infer sync from attachment alone.

3. Expecting attachment to filter get(). Steer authentication via transports and the credential list instead.


Related