logo svg
logo

October 24, 2025

How Do Passkeys Work for Passwordless Auth

Learn how passkeys use public key cryptography and WebAuthn to replace passwords. See registration, login flow, risks, code, and best practices for rollout.

Khaled Hassan

Khaled Hassan

Featured Image

Introduction

Passwords fail often. Users forget them. Attackers phish them. Breached databases leak them. The result is lockouts, fraud, and high support cost. Passkeys promise a better path. They remove shared secrets from servers and replace them with public key cryptography tied to the user’s device. The login becomes a quick local check, a fingerprint or face scan or PIN, then a signed challenge to the site. No typing. No SMS codes. No phishable links.

In this guide, you will learn how passkeys work end to end. We start with plain definitions, then walk the full flow for registration and sign in. You will see the moving parts, authenticators, relying parties, key pairs, and what each one does. We compare validation and verification since teams often confuse those steps. We cover risks and pitfalls, such as RP ID mismatches and device loss, with fixes that you can ship today. We include code that you can drop into a small demo, plus a mini case study from the field. Finally, we share best practices that map to real production, rate limits, timeouts, error paths, and privacy notices. If you want a clear answer to “how do passkeys work,” plus a path to deploy, this article is for you.

Sources for claims and figures are cited inline. For example, WebAuthn Level 3 defines the API and scoping rules, and platform vendors document passkey security and recent updates.

Definition box

Passkeys are FIDO based credentials that replace passwords. A site stores a public key. The user’s device stores a private key and protects it with a local unlock method, such as biometrics or a PIN. Sign in is a signed challenge, not a shared secret.

How it works, step by step

We will use the WebAuthn terms: relying party (your site), authenticator (the device or security key), and user.

Registration

  1. The site creates a random challenge and sends a navigator.credentials.create() request with parameters such as RP ID, user handle, and acceptable algorithms. W3C
  2. The device prompts the user to unlock.
  3. The authenticator generates a key pair scoped to the RP ID. The private key stays on the device. The public key and an attestation object return to the site. W3C
  4. The site validates the response and stores the public key with the user’s account.

Authentication

  1. The site creates a new challenge and calls navigator.credentials.get() with the RP ID and allow list.
  2. The user unlocks the device.
  3. The authenticator signs the challenge and origin data with the stored private key.
  4. The site verifies the signature using the stored public key and checks the origin, RP ID, challenge, and counter.

Reference, WebAuthn Level 3 formalizes creation and use of public key credentials, scoped to the RP and origin. W3C

[User] unlocks device
   ↓ local biometrics or PIN
[Authenticator] creates key pair or signs challenge
   ↓
[Browser WebAuthn] packages response
   ↓ HTTPS
[Relying Party] validates or verifies and updates session

Anatomy of a passkey

Breakdown of the core objects you will see in code and logs.

Pseudo example, server record

json
{
  "userHandle": "01F4C2...",
  "rpId": "example.com",
  "credentials": [
    {
      "credId": "A1b2C3...",
      "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkq...\n-----END PUBLIC KEY-----",
      "signCount": 12,
      "addedAt": "2025-10-24T12:00:00Z",
      "attestation": "none"
    }
  ]
}

Validation vs verification

Teams mix these terms. Keep them separate.

When to use which

AspectValidation, registrationVerification, authentication
GoalTrust the new credentialTrust the current login
InputsAttestation, public key, challenge, originChallenge, origin, authenticator data, signature
OutputStore credential, link to userIssue session or token
Typical failureRP ID mismatch, bad attestationReplay, wrong origin, bad signature

Specification, WebAuthn defines both the creation ceremony and assertion ceremony, with conformance requirements for RP ID scope and origin checks.

Common pitfalls and security risks

Callout note, header size

Do not stuff binary WebAuthn responses into headers. They are larger than typical limits. Use JSON bodies over HTTPS. Keep challenge payloads small. This avoids 413 or 431 errors on some CDNs and WAFs.

Implementation guide

Below is a thin, working flow that you can adapt. It uses WebAuthn with a typical Node backend. Code examples are short and valid.

passkeys-demo/
├── server/
│   ├── index.js
│   └── verify.js
├── web/
│   ├── register.js
│   └── signin.js
└── README.md

1) Server, create registration options

// server/index.js
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.json());

const rpId = "example.com";
const users = new Map(); // userHandle -> { credentials: [] }

app.post("/register/options", (req, res) => {
  const { userHandle, username } = req.body;
  const challenge = crypto.randomBytes(32);
  res.json({
    rp: { id: rpId, name: "Example Inc." },
    user: {
      id: Buffer.from(userHandle, "hex"),
      name: username,
      displayName: username
    },
    challenge: challenge.toString("base64url"),
    pubKeyCredParams: [{ type: "public-key", alg: -7 }, { type: "public-key", alg: -257 }],
    attestation: "none",
    timeout: 60000
  });
});

2) Client, call navigator.credentials.create

// web/register.js
async function register(userHandle, username) {
  const opts = await fetch("/register/options", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ userHandle, username })
  }).then(r => r.json());

  opts.challenge = Uint8Array.from(atob(opts.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
  opts.user.id = Uint8Array.from(opts.user.id.data || opts.user.id); // handle Buffer serialization

  const cred = await navigator.credentials.create({ publicKey: opts });
  // Send to server for validation and storage
  const payload = {
    id: cred.id,
    rawId: Array.from(new Uint8Array(cred.rawId)),
    response: {
      clientDataJSON: Array.from(new Uint8Array(cred.response.clientDataJSON)),
      attestationObject: Array.from(new Uint8Array(cred.response.attestationObject))
    },
    type: cred.type
  };
  await fetch("/register/verify", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
}

3) Server, validate and store public key

Use a well tested library in production. Pseudo code below shows the checks that must pass, challenge, origin, RP ID, and attestation policy.

// server/verify.js
import { validateRegistrationResponse } from "@simplewebauthn/server"; // example library

app.post("/register/verify", async (req, res) => {
  const expectedOrigin = "https://example.com";
  const expectedRPID = rpId;

  const verification = await validateRegistrationResponse({
    response: req.body,
    expectedOrigin,
    expectedRPID,
    requireUserVerification: true
  });

  if (!verification.verified) return res.status(400).json({ ok: false });

  const { credentialID, credentialPublicKey, counter, userHandle } = verification.registrationInfo;
  const record = users.get(userHandle) || { credentials: [] };
  record.credentials.push({ credId: Buffer.from(credentialID).toString("base64url"), publicKey: credentialPublicKey, signCount: counter });
  users.set(userHandle, record);
  res.json({ ok: true });
});

4) Authentication options and navigator.credentials.get

// server/index.js
app.post("/auth/options", (req, res) => {
  const { userHandle } = req.body;
  const user = users.get(userHandle);
  if (!user) return res.status(404).end();
  const challenge = crypto.randomBytes(32);
  const allowCredentials = user.credentials.map(c => ({
    type: "public-key",
    id: Buffer.from(c.credId, "base64url")
  }));
  res.json({ challenge: challenge.toString("base64url"), rpId, allowCredentials, userVerification: "required", timeout: 30000 });
});
// web/signin.js
async function signIn(userHandle) {
  let opts = await fetch("/auth/options", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userHandle }) }).then(r => r.json());
  opts.challenge = Uint8Array.from(atob(opts.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
  opts.allowCredentials = opts.allowCredentials.map(c => ({ ...c, id: Uint8Array.from(c.id.data || c.id) }));
  const assertion = await navigator.credentials.get({ publicKey: opts });
  const payload = {
    id: assertion.id,
    rawId: Array.from(new Uint8Array(assertion.rawId)),
    response: {
      clientDataJSON: Array.from(new Uint8Array(assertion.response.clientDataJSON)),
      authenticatorData: Array.from(new Uint8Array(assertion.response.authenticatorData)),
      signature: Array.from(new Uint8Array(assertion.response.signature)),
      userHandle: assertion.response.userHandle ? Array.from(new Uint8Array(assertion.response.userHandle)) : null
    },
    type: assertion.type
  };
  await fetch("/auth/verify", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
}
// server/verify.js
import { verifyAuthenticationResponse } from "@simplewebauthn/server";

app.post("/auth/verify", async (req, res) => {
  const expectedOrigin = "https://example.com";
  const expectedRPID = rpId;

  // Look up public key by credId
  const credId = req.body.id;
  const user = [...users.values()].find(u => u.credentials.find(c => c.credId === credId));
  if (!user) return res.status(404).end();
  const cred = user.credentials.find(c => c.credId === credId);

  const verification = await verifyAuthenticationResponse({
    response: req.body,
    expectedOrigin,
    expectedRPID,
    authenticator: { credentialPublicKey: cred.publicKey, credentialID: Buffer.from(cred.credId, "base64url"), counter: cred.signCount }
  });

  if (!verification.verified) return res.status(401).json({ ok: false });
  cred.signCount = verification.authenticationInfo.newCounter;
  res.json({ ok: true, session: "new-session-id" });
});

Platform notes

Performance and limits

Best practices checklist

Use cases and anti-patterns

Fits well

Avoid or use alternatives

Alternatives

For deeper help, see DeepStrike’s penetration testing services to pressure test authentication and recovery paths, and to model phishing resistance in the real world. FIDO Alliance
Internal link, learn more about security testing at DeepStrike:

Conclusion

Passkeys remove passwords and their failures, phishing, reuse, and weak secrets. They shift trust to strong cryptography that is bound to your site and to the user’s device. The login becomes a short local unlock and a signed challenge. Teams get higher success rates, faster sign ins, and a simpler stack. The rollout does need care, RP ID scope, recovery planning, and clear UX. Start with a hybrid phase, measure success, and expand to admin consoles first. Modern platforms document policy and deployment steps, and the WebAuthn Level 3 spec shows exactly what to validate and verify. If you need help pressure testing your rollout, consider a focused engagement to simulate attacks and recovery edge cases. Learn more at DeepStrike’s penetration testing services, then build a plan and ship.

Common FAQs

Are passkeys the same as FIDO2 or WebAuthn?
Passkeys are user friendly packaging of FIDO2 WebAuthn credentials. Same crypto, better UX. FIDO Alliance

Do I still need passwords?
You can keep them as a fallback. The goal is to phase them out as device support grows. Many platforms allow both during transition. The LastPass Blog

What about cross device use?
Platform managers sync passkeys with end to end encryption. Apple and Google shipped improvements in 2025 to make this smoother. Apple Developer

Are passkeys phishing resistant?
Yes. Credentials are bound to the origin. A fake site cannot produce a valid signature that matches your RP ID and origin checks. W3C

How fast are passkeys in practice?
Public data shows higher success and faster sign ins at large scale, including Google reporting 30 percent higher success and 20 percent faster sign ins. FIDO Alliance

Do they work on Windows, macOS, iOS, and Android?
Yes, all major vendors support passkeys and document setup and policy

background
Let's hack you before real hackers do

Stay secure with DeepStrike penetration testing services. Reach out for a quote or customized technical proposal today

Contact Us