Passkeys and WebAuthn for Frontend Authentication

Passkeys are a modern, phishing-resistant sign-in method built on WebAuthn (Web Authentication). Users approve sign-in with a local unlock mechanism such as Face ID, Touch ID, Windows Hello, Android screen lock, a device PIN, or a hardware security key instead of typing a reusable password.
The security model is public-key cryptography, not magic biometrics. During registration, an authenticator creates a key pair for a specific relying party. The private key stays with the authenticator. The server stores the public key. During authentication, the server sends a fresh challenge, the authenticator signs it with the private key, and the server verifies the signature with the stored public key.
Frontend engineers do not verify passkeys. They orchestrate the ceremony: fetch challenge/options from the server, call navigator.credentials.create() or navigator.credentials.get(), handle Conditional UI/autofill, cancellation, unsupported browsers, and fallback UX, then send the browser response back to the backend. The backend owns challenge lifecycle, relying-party ID and origin checks, signature verification, credential binding, and session creation.
Quick Navigation: Mental Model: Ceremony, Not Password Replacement UI • Registration: Creating a Passkey • Authentication: Signing a Challenge • What to Use in a Real Frontend • Conditional UI and Autofill • Why Passkeys Resist Phishing • Fallback, Recovery, and Platform Variability • Common Implementation Mistakes
Mental Model: Ceremony, Not Password Replacement UI
| Actor | Responsibility |
|---|---|
| Relying Party | the website/service that authenticates the user |
| Frontend | requests options, invokes WebAuthn, handles UX states, sends responses back |
| Browser / client platform | exposes the Credential Management and WebAuthn APIs |
| Authenticator | platform authenticator, credential manager, or security key that protects private keys |
| Backend | issues challenges, validates responses, stores public keys, creates sessions |
Registration: Creating a Passkey
Registration happens during sign-up, account setup, or "add passkey" from account settings.
server options -> navigator.credentials.create() -> authenticator creates key pair -> server verifies and stores public keyTypical frontend shape:
const options = await fetch('/api/passkeys/register/options').then((r) => r.json())
const credential = await navigator.credentials.create({
publicKey: options,
})
await fetch('/api/passkeys/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
})In production, raw WebAuthn objects contain binary values such as ArrayBuffer, so teams usually use a helper library or native JSON conversion helpers where supported. The frontend should not hand-roll cryptography; it should preserve the response shape and send it to a server verifier.
Server verification must check the challenge, relying-party identity, origin, credential data, attestation policy if used, and account binding before storing the public key.
Authentication: Signing a Challenge
Authentication proves possession of the private key without sending the private key to the server.
server challenge -> navigator.credentials.get() -> authenticator signs challenge -> server verifies assertion -> sessionTypical frontend shape:
const options = await fetch('/api/passkeys/login/options').then((r) => r.json())
const assertion = await navigator.credentials.get({
publicKey: options,
})
await fetch('/api/passkeys/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(assertion),
})The interview distinction: navigator.credentials.get() is not "logging in". It asks the browser/authenticator for a signed assertion. Login happens only after the backend verifies the assertion and creates an application session.
What to Use in a Real Frontend
| Need | Practical choice | Why |
|---|---|---|
| Raw browser capability | navigator.credentials.create() and navigator.credentials.get() | official WebAuthn entry points |
| Frontend response formatting | @simplewebauthn/browser or equivalent | avoids brittle ArrayBuffer / base64url conversion code |
| Backend verification | server-side WebAuthn library for your stack | verification belongs on the server, not in React |
| Browser support and UX recipes | MDN, web.dev, Chrome Developers, passkeys.dev | keep behavior aligned with platform guidance |
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
async function addPasskey() {
const optionsJSON = await fetch('/api/passkeys/register/options').then((r) => r.json())
const response = await startRegistration({ optionsJSON })
await fetch('/api/passkeys/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(response),
})
}
async function signInWithPasskey() {
const optionsJSON = await fetch('/api/passkeys/login/options').then((r) => r.json())
const response = await startAuthentication({ optionsJSON })
await fetch('/api/passkeys/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(response),
})
}A staff-level answer should name the tool without confusing it for the security model. Libraries reduce encoding and ceremony mistakes; they do not remove the backend verification boundary.
Conditional UI and Autofill
The best passkey UX often feels like normal username/password autofill. Conditional UI lets the browser offer passkeys from the username field, alongside saved credentials, without forcing users to choose an auth method before they know what exists.
<input name="username" autocomplete="username webauthn" />const supportsConditionalUI =
'PublicKeyCredential' in window &&
typeof PublicKeyCredential.isConditionalMediationAvailable === 'function' &&
await PublicKeyCredential.isConditionalMediationAvailable()
if (supportsConditionalUI) {
const assertion = await navigator.credentials.get({
publicKey: requestOptionsFromServer,
mediation: 'conditional',
})
// Send assertion to the server for verification.
}Practical rules:
autocomplete="username webauthn" intactNotAllowedError as normal UX outcomesConditional UI improves adoption. It is not a replacement for fallback, recovery, or explicit account-management controls.
Why Passkeys Resist Phishing
Passkeys resist phishing because credentials are scoped to the relying party. A credential created for example.com cannot simply be replayed from examp1e.com.
Cause and effect:
Common misconception: biometrics are not sent to the server. Face or fingerprint unlock is local authenticator UX. The server receives WebAuthn response data and verifies cryptographic proof.
Fallback, Recovery, and Platform Variability
| Scenario | Product requirement |
|---|---|
| unsupported browser or embedded webview | keep another sign-in path |
| lost phone or security key | allow recovery without weakening the account model |
| multiple devices | support multiple passkeys per account |
| passkey deleted on server but still offered by browser | handle unknown credential states cleanly |
| enterprise or shared-device environments | support policy-driven fallback |
Common Implementation Mistakes
The sharp interview line: passkeys reduce password risk, but the surrounding system still needs secure recovery, session management, device management, and abuse controls.
Useful Resources and Libraries
navigator.credentials.create(), navigator.credentials.get(), challenges, and server validation boundaries.autocomplete="username webauthn", and autofill UX.Use official browser docs as correctness sources. Use libraries to reduce implementation mistakes, not to move trust decisions into the frontend.
Senior Interview Answer Pattern
| Prompt | Strong response |
|---|---|
| Are biometrics sent to the server? | No. Biometrics or PIN unlock the authenticator locally. |
| Why are passkeys phishing-resistant? | Credentials are scoped to the relying party/origin and do not expose a reusable secret. |
| Does Conditional UI replace a passkey button? | No. It improves autofill UX; explicit actions and fallback still matter. |
| Is frontend verification enough? | No. The backend verifies challenge, origin, relying-party ID, signature, credential ID, and account binding. |
| Do passkeys remove account recovery? | No. Recovery becomes a core security design decision. |
Key Takeaways
navigator.credentials.create() and navigator.credentials.get(); backend code verifies trust.autocomplete="username webauthn" and mediation: "conditional", but fallback still matters.@simplewebauthn/browser for response formatting instead of brittle binary conversion glue.