Passkey Registration
This guide details how to enable users to register a passkey for their account, providing a highly secure, phishing-resistant passwordless sign-in alternative.
Database Requirements
Section titled “Database Requirements”To support passkey registrations, your database credential table must store the following fields:
export interface StoredPasskeyCredential { id: string; // Base64URL-encoded credential ID (unique lookup key) passkeyUserId: string; // Associated application user ID credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures credentialType: "public-key"; credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys credentialBackedUp: boolean; // Boolean backup state reported by the authenticator aaguid: string; // Authenticator Attestation GUID providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs) name: string; // Provider name derived from AAGUID registry transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options lastUsedAt?: number; // Optional epoch timestamp of last sign-in registeredAt: number; // Registration epoch timestamp counter: number; // Authenticator sign-in signature counter used to prevent replay attacks}Server-Side
Section titled “Server-Side”Options Generation
Section titled “Options Generation”Create an endpoint that generates WebAuthn creation parameters. Rely on a vetted library per category standards instead of hand-rolling cryptography.
- Use the predefined RP ID: Use the predefined proper RP ID as a constant string in
rp.idso the client can passpublicKey.rp.iddirectly to Signal API calls. - Create a secure Challenge: Generate a high-entropy, cryptographically secure random buffer on the server, store it securely in the user’s session, and encode it as Base64URL for options delivery.
- Avoid Duplicate Passkeys: Map the user’s existing pre-registered credential IDs to the
excludeCredentialsoptions array. This prevents the authenticator from registering duplicate credentials on the same passkey provider account. - Enforce Discoverable Credentials: Set
requireResidentKey: trueandresidentKey: "required"in theauthenticatorSelectionoptions to request a discoverable credential, which is necessary for discoverable sign-ins. - Configure User Verification: Specify
userVerification: "preferred"oruserVerification: "required". Many compliance use cases (e.g., finance, healthcare) require'required'to enforce user verification on creation. - Determine Attachment Scope:
- Promotion Flow: When proposing passkey creation right after standard password sign-ins or post-signup promotions, set
authenticatorAttachment: "platform"to enforce platform authenticator and bypass external security key prompts. - Management Flow: When called from a dedicated settings or security panel where external security keys are supported in addition to platform authenticator, omit the
authenticatorAttachmentproperty entirely. - Tip: Accept a
promotion: booleanrequest flag to conditionally handle both flows with a single endpoint.
- Promotion Flow: When proposing passkey creation right after standard password sign-ins or post-signup promotions, set
// Options generation exampleconst options = { challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge rp: { id: "example.com", name: "Secure Application" }, user: { id: userBase64UrlId, // Unique base64url string identifying the account displayName: "Jane Doe", }, pubKeyCredParams: [ { type: "public-key", alg: -7, }, { type: "public-key", alg: -257, }, ], excludeCredentials: userExistingCredentials.map((cred) => ({ type: "public-key", id: cred.id, transports: cred.transports, })), authenticatorSelection: { residentKey: "required", requireResidentKey: true, userVerification: "preferred", ...(isPromotionFlow && { authenticatorAttachment: "platform" }), },};Verification
Section titled “Verification”- Challenge Verification: Securely verify the challenge against the expected session bound challenge.
- Verify User Presence:
- Ensure that the User Present (UP) flag returned in the parsed authenticator data is
trueto confirm physical user presence at the time of creation.
- Ensure that the User Present (UP) flag returned in the parsed authenticator data is
- Relaxing Verification for ‘preferred’:
- When the creation options specified
userVerification: "preferred", the server-side verification call MUST be configured withrequireUserVerification: false. Otherwise, authenticators that register without user verification (e.g., screen locks disabled) will trigger spurious server verification failures.
- When the creation options specified
Client-Side Logic
Section titled “Client-Side Logic”- Gate the UI on page load:
- On page load, call
PublicKeyCredential.getClientCapabilities()and disable the “Create passkey” button ifconditionalGetorpasskeyPlatformAuthenticatoris not available.
- On page load, call
- Invoke creation & Serialize: Decode server options with
PublicKeyCredential.parseCreationOptionsFromJSON()and pass the resulting configuration tonavigator.credentials.create().- Call
credential.toJSON()to encode theAuthenticatorAttestationResponseinto a valid, JSON-serializable object before fetching the verification endpoint.
- Call
- Handle WebAuthn Exceptions:
InvalidStateError: A matching passkey already exists (matched byexcludeCredentials).NotAllowedError: The user cancelled or timed out the authentication passkey dialog.AbortError: The operation has been aborted.SecurityError: Secure origins (HTTPS) or RP ID mismatch errors (configuration issues).
- Try/Catch Segregation for Signal API:
- Wrap server verification
fetch()call in a try/catch block. CallPublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rp.id, credentialId: encodedResponse.id })(gated onif (PublicKeyCredential.signalUnknownCredential)) when the server verification fetch fails (any statusresponse.ok === falseor network throws). - Always read
rpIdfrompublicKey.rp.idin the server’s creation options. DO NOT deriverpIdfromwindow.location.hostnameon the client, as the server’s configured RP ID may be a registrable parent domain (such asexample.comonaccounts.example.com) or use Related Origin Requests.
- Wrap server verification
// optionsFetch and registerVerifyFetch are app-defined HTTP methodsimport { optionsFetch, registerVerifyFetch } from "./api.js";
async function registerPasskey(isPromotion = false) { // Verify passkey capability and conditional UI are available const capabilities = await PublicKeyCredential.getClientCapabilities(); if ( !capabilities.passkeyPlatformAuthenticator || !capabilities.conditionalGet ) { // Hide "Create passkey" buttons and fall back to password flows instead showStandardPasswordFallbackUI(); return; }
const creationOptionsJSON = await optionsFetch({ promotion: isPromotion }); const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);
let credential; try { // passkey prompt execution credential = await navigator.credentials.create({ publicKey }); } catch (err) { if (err.name === "InvalidStateError") { console.log("A passkey already exists for this account."); alert("A passkey already exists for this account."); } else if (err.name === "SecurityError") { console.error("Configuration RP ID or Secure Context error."); alert("Configuration RP ID or Secure Context error."); } else if (err.name === "NotAllowedError") { console.log("User cancelled the passkey dialog."); } else if (err.name === "AbortError") { console.log("The creation operation has been aborted."); } return; // Safe API exit, do not signal unknown for standard WebAuthn cancels }
// Server Verification phase (Segregated Try/Catch) const encodedResponse = credential.toJSON(); try { const response = await registerVerifyFetch(encodedResponse); if (!response.ok && PublicKeyCredential.signalUnknownCredential) { // Server verification failed to verify/authenticate the credential (orphaned) await PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rp.id, // Always use the server-provided rp.id; DO NOT use window.location.hostname credentialId: encodedResponse.id, // Base64URL-encoded credential ID }); } } catch (serverErr) { console.error("Server verification network failure:", serverErr); if (PublicKeyCredential.signalUnknownCredential) { await PublicKeyCredential.signalUnknownCredential({ rpId: publicKey.rp.id, // Always use the server-provided rp.id; DO NOT use window.location.hostname credentialId: encodedResponse.id, // Base64URL-encoded credential ID }); } }}Fallback Strategies
Section titled “Fallback Strategies”Signal API Synchronization Fallback
Section titled “Signal API Synchronization Fallback”Baseline: webauthn-signals
The WebAuthn Signal API (webauthn-signals) is a progressive optimization used to keep password managers in sync with the server credential state.
- Fallback Experience: If
PublicKeyCredential.signalUnknownCredentialis unsupported by the browser, the call MUST be bypassed safely via feature detection gating (if (PublicKeyCredential.signalUnknownCredential)), and the server-side verification simply logs the failure without triggering manager updates.
Easy JSON Serialization Fallback
Section titled “Easy JSON Serialization Fallback”Baseline: webauthn / api.PublicKeyCredential.parseCreationOptionsFromJSON_static
Load webauthn-polyfills so PublicKeyCredential.parseCreationOptionsFromJSON() and PublicKeyCredential.prototype.toJSON() are available whenever PublicKeyCredential is supported:
// With a bundler (`npm install webauthn-polyfills`):import 'webauthn-polyfills';Without a bundler, serve the package from your own origin, or import it from a CDN such as https://esm.sh/[email protected] (pin the exact version). For third-party URLs, add Subresource Integrity: the integrity attribute on the <script> tag, or an import map’s integrity field for import statements.
Compatibility notes: Badges like Baseline: … mark web features referenced by this guide. Baseline status describes availability across the core browser set — it does not mean every browser or older version supports the feature. Feature data as published upstream, synced 2026-11-12.
This demo needs browser permissions or external resources, so it is not embedded. Open the demo as a standalone page (it runs in your browser; some features may require a supported browser and your permission).
Counter-example (negative demo) — what this guide tells you to avoid.
Source: GoogleChrome/modern-web-guidance-src (Apache-2.0), upstream commit 179dbc6, synced 2026-11-12. Local changes: macros expanded to links/badges, first heading de-duplicated, demo and this footer added. This is an independent, unofficial site — not a Google publication.