Pilcrow

Auth book

Passkey registration

Passkeys are discoverable WebAuthn credentials that use user verification and do not require attestation.

Because attestation is not required, providing a challenge is optional. You can simply pass an empty byte array as the challenge.

Algorithms -7 (ES256) and -257 (RS256) must be supported. ES256 is by far the most widely supported algorithm. RS256 is required for compatibility with older Windows devices, which only supported RSA credentials. Even devices running newer versions of Windows may still only suppport RS256 if the user created their passkeys before updating. You can also optionally support algorithm -8 (EdDSA), which is widely supported by security keys.

const credential = await navigator.credentials.create({
	challenge: new Uint8Array(0),
	rp: {
		id: new URL(window.location.href).hostname,
	},
	user: {
		id: new TextEncoder().encode(userId),
		displayName: userEmailAddress,
	},
	pubKeyCredParams: [
		{ type: "public-key", alg: -8 },
		{ type: "public-key", alg: -7 },
		{ type: "public-key", alg: -257 },
	],
	excludeCredentials: [{
		id: existingCredentialId,
		type: "public-key",
	}],
	authenticatorSelection: {
		residentKey: "required",
		requireResidentKey: true,
		userVerification: "required",
	},
	attestation: "none",
	extensions: {
		credentialProtectionPolicy: "userVerificationRequired",
		enforceCredentialProtectionPolicy: false,
	},
})

Optionally, set the credentialProtectionPolicy extension input to "userVerificationRequired". This prevents credential enumeration on roaming authenticators such as security keys, and also requires user verification even when a credential ID is supplied. If you plan to make user verification optional when the passkey is used as a second factor, use "userVerificationOptionalWithCredentialIDList" instead. enforceCredentialProtectionPolicy must be set to false to avoid blocking devices that do not support the extension, such as software-based authenticators. See my blog Understanding WebAuthn credential protection policy for more details on the credential protection policy.

The public key must be properly validated before storage:

The extensions data in the authenticator data is usually empty unless you explicitly pass extension inputs to navigator.credentials.create(). However, Chrome automatically includes the credentialProtectionPolicy extension input unless it is already defined, so extension data may still be included even if you didn't provide any extensions yourself. If defined, the CBOR map will contain a credProtect key with one of three values: integer 1 for userVerificationOptional, integer 2 for userVerificationOptionalWithCredentialIDList, and integer 3 for userVerificationRequired. If you set the extension input to "userVerificationRequired", you can simply check that the extension data equals:

A16B6372656450726F7465637403

Note that even if you explicitly set the extension input, credential protection policy support is limited to roaming authenticators that implement the latest standard. Software-based authenticators, such as password managers, generally do not support it and will not include extensions data in the authenticator data. As such, you must also allow authenticator data that does not include extensions data.