fix(auth): require user verification on every passkey ceremony

`generateRegistrationOptions` and `generateAuthenticationOptions` were
both passing `userVerification: "preferred"`. The WebAuthn spec
treats `preferred` as a soft hint — an authenticator that lacks (or
chooses to skip) a UV factor returns an assertion with
`userVerified: false`, and SimpleWebAuthn accepts it as a successful
ceremony. The registration carries the same `userVerified: false`
flag forever, so a credential created without a biometric / PIN /
fingerprint check cannot reach AAL3 on any later assertion either.

Flipping both call sites to `"required"` makes the UV check
mandatory at the authenticator-policy layer. Apple's platform
authenticators (Touch ID / Face ID / passcode) and every shipped
hardware authenticator support UV, so the user-visible flow is
unchanged for every realistic device.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-27 07:39:51 +02:00
1 parent b89a79554a
commit a680785023
1 file changed
+12 -2
+12 -2
View File
@@ -140,7 +140,13 @@ export async function createRegistrationOptions(
})),
authenticatorSelection: {
residentKey: "preferred",
userVerification: "preferred",
// `required` (vs `preferred`) instructs the authenticator that
// a UV check (biometric / PIN / fingerprint) is mandatory for
// every registration ceremony. A `preferred` posture lets a
// device that lacks a UV factor silently downgrade — and the
// resulting passkey carries the same `userVerified: false` flag
// forever, blocking any future AAL3-style assertion against it.
userVerification: "required",
},
});
@@ -216,7 +222,11 @@ export async function createAuthenticationOptions(userId?: string) {
const options = await generateAuthenticationOptions({
rpID: getRpId(),
userVerification: "preferred",
// Match the registration posture — see `createRegistrationOptions`
// above for the rationale. Refusing to honour a `userVerified:
// false` assertion keeps the AAL3 floor that the registration
// policy now enforces.
userVerification: "required",
allowCredentials,
});