Passkeys and WebAuthn for Frontend Authentication

Medium•
Comic-style Passkeys and WebAuthn flow showing registration, authenticator, private key staying local, public key stored on server, login challenge, signed challenge, and verified user.

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.

Mental Model: Ceremony, Not Password Replacement UI

ActorResponsibility
Relying Partythe website/service that authenticates the user
Frontendrequests options, invokes WebAuthn, handles UX states, sends responses back
Browser / client platformexposes the Credential Management and WebAuthn APIs
Authenticatorplatform authenticator, credential manager, or security key that protects private keys
Backendissues 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 key

Typical 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 -> session

Typical 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

NeedPractical choiceWhy
Raw browser capabilitynavigator.credentials.create() and navigator.credentials.get()official WebAuthn entry points
Frontend response formatting@simplewebauthn/browser or equivalentavoids brittle ArrayBuffer / base64url conversion code
Backend verificationserver-side WebAuthn library for your stackverification belongs on the server, not in React
Browser support and UX recipesMDN, web.dev, Chrome Developers, passkeys.devkeep 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:

•keep the username field and autocomplete="username webauthn" intact
•start conditional authentication only after you have server request options
•do not block password, single sign-on, or recovery while the conditional promise waits
•provide an explicit "Sign in with passkey" action for users who expect a visible button
•treat cancellation, timeout, and NotAllowedError as normal UX outcomes

Conditional 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:

•the user does not type a reusable secret into a form
•the private key is not sent over the network
•the authenticator signs a fresh challenge for the relying party
•the server verifies the challenge, origin, relying-party ID, signature, and credential binding

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

ScenarioProduct requirement
unsupported browser or embedded webviewkeep another sign-in path
lost phone or security keyallow recovery without weakening the account model
multiple devicessupport multiple passkeys per account
passkey deleted on server but still offered by browserhandle unknown credential states cleanly
enterprise or shared-device environmentssupport policy-driven fallback

Common Implementation Mistakes

•Treating frontend success as authentication success.
•Reusing challenges or allowing long-lived challenges.
•Skipping origin, relying-party ID, or account-binding checks on the server.
•Assuming every passkey is synced across devices.
•Assuming every passkey is discoverable in the same way.
•Building only a passkey button and ignoring Conditional UI/autofill.
•Building only Conditional UI and ignoring explicit fallback.
•Making password recovery weaker than the previous password-only system.
•Forgetting secure-context and browser-support constraints.

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

•MDN Web Authentication API: browser API model, secure-context requirement, navigator.credentials.create(), navigator.credentials.get(), challenges, and server validation boundaries.
•MDN Passkeys: passkey model, scope, origin verification, security properties, lost-passkey handling, and migration guidance.
•web.dev passkey form autofill: Conditional UI, autocomplete="username webauthn", and autofill UX.
•Chrome Developers WebAuthn Conditional UI: browser UX details for passkey autofill.
•SimpleWebAuthn Browser: frontend helpers for request/response formatting.
•passkeys.dev: implementation recipes and ecosystem guidance.

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

PromptStrong 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

1Passkeys are WebAuthn credentials backed by public-key cryptography, not passwords hidden behind biometrics.
2Registration creates a key pair for a relying party; authentication signs a fresh server challenge.
3The private key stays with the authenticator; the server stores the public key and verifies signatures.
4Frontend code orchestrates navigator.credentials.create() and navigator.credentials.get(); backend code verifies trust.
5Conditional UI/autofill improves adoption through autocomplete="username webauthn" and mediation: "conditional", but fallback still matters.
6Use helper libraries such as @simplewebauthn/browser for response formatting instead of brittle binary conversion glue.
7Biometrics and device PINs are local authenticator UX; they are not sent to the server.
8Staff-level answers include recovery, platform variability, passkey management, and server-side validation boundaries.