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
- Platform vs Roaming Authenticator Trade-offs — the parent decision guide and comparison matrix
- Hybrid Transport and Cross-Device Passkeys — how a phone answers a
cross-platformrequest over BLE - Discoverable vs Server-Side Credentials — how
residentKeypairs with attachment