Authentication And Identity Systems

Hardware Keys, Passkeys, And WebAuthn

Public-Key Authentication, Not Shared Secrets

WebAuthn replaces shared secrets with public-key cryptography. During registration, the user's device (an authenticator) generates a key pair unique to that site: the private key never leaves the device, and the server stores only the public key. At login, the server sends a random challenge; the authenticator signs it with the private key; the server verifies the signature with the stored public key.

PHP example
<?php

declare(strict_types=1);

$model = [
    'registration' => 'authenticator makes a keypair; server stores the PUBLIC key',
    'login'        => 'server sends a random challenge',
    'proof'        => 'authenticator signs the challenge with the PRIVATE key',
    'verify'       => 'server checks the signature against the stored public key',
];

foreach ($model as $phase => $what) {
    echo $phase . ': ' . $what . PHP_EOL;
}

// Prints:
// registration: authenticator makes a keypair; server stores the PUBLIC key
// login: server sends a random challenge
// proof: authenticator signs the challenge with the PRIVATE key
// verify: server checks the signature against the stored public key

Two consequences fall out immediately. A server breach leaks only public keys, which are useless to an attacker — there is no password-equivalent secret to steal, so there is nothing to crack or reuse. And there is no secret for the user to type, forward, or be phished out of.

Why It Resists Phishing

The phishing resistance is not a policy; it is built into the protocol. Each credential is bound to the origin (the exact domain) it was registered for. The browser will only invoke a credential for the site that created it, so a user on paypa1-security.com simply has no credential to offer — the authenticator refuses, because the origin does not match. The signed challenge also includes the origin, so even a relay attack cannot repackage the signature for the real site. Compare this to SMS or TOTP, where a fake page just asks the user to type the code and relays it: WebAuthn makes that attack impossible rather than merely discouraged.

Passkeys Versus Hardware Keys

The underlying protocol is the same; the difference is where the private key lives.

  • A hardware security key (YubiKey and similar) stores the key on a dedicated physical device you carry and tap. Highest assurance, and the credential cannot be copied off the device — favored for high-security and enterprise use.
  • A passkey stores the credential in the platform (Apple, Google, Microsoft) and syncs it across the user's devices through their cloud account. This solves WebAuthn's historic adoption problem — losing your one hardware key used to mean lockout — by making credentials recoverable and available on every device the user owns. The trade is that the credential's security now also rests on the platform account, but for the vast majority of users, synced passkeys are both far stronger than passwords and finally convenient enough to adopt.

Passkeys can also serve as a single factor that is stronger than a password plus SMS, because the device unlock (biometric or PIN) plus possession of the device already constitutes multiple factors.

Implementing In PHP

You will not hand-roll the cryptography — use a maintained WebAuthn library (such as web-auth/webauthn-lib). Your responsibilities are the server-side plumbing: generate and store a random challenge per ceremony, verify the library's response, and persist each credential (its ID and public key) against the user, since a user may register several authenticators. Because passkeys are still newer to most users, ship them alongside an existing method and let users opt in, rather than forcing a cutover — the account-linking model from earlier in this track (one user, many credentials) is exactly the shape you need. Always provide a recovery path that does not itself reintroduce a phishable weakness as the account's true weakest link.

What To Check

Before moving on, make sure you can:

  • explain the public-key model and why a server breach leaks nothing useful
  • explain origin binding and why it makes phishing structurally impossible
  • distinguish hardware keys from synced passkeys and their trade-offs
  • explain why a passkey can be a strong single factor
  • describe the PHP server's role: challenges, verification via a library, per-credential storage
  • ensure the recovery path does not become the weakest link

What You Should Be Able To Do

After this lesson, you should be able to integrate WebAuthn into a PHP app with a library, explain to stakeholders why passkeys are phishing-resistant where SMS and TOTP are not, offer passkeys alongside existing methods, and design a recovery path that preserves rather than undermines their security.

Practice

Practice: Add Passkeys To An App
Show solution

Registration ceremony: an authenticated user opts in from settings. The server generates a random challenge, stores it in the session, and passes the relying-party info to the browser API. The authenticator creates an origin-bound keypair; the server verifies the library's attestation response and stores the credential ID and public key against the user. A user may register several (laptop passkey, phone passkey, a hardware key), so storage is one-user-to-many-credentials — the same identity model as social login.

Login ceremony: the server sends a fresh random challenge; the authenticator signs it with the private key; the server verifies the signature against the stored public key and checks the challenge and origin. No shared secret is transmitted.

Coexistence: passkeys are offered alongside password/SMS, not as a forced cutover. Users opt in, and once they have a passkey it can serve as a strong single factor (device unlock + possession), letting them skip password+SMS. The app steers high-value accounts toward passkeys as the phishing-resistant option.

Recovery: at least one recovery path that is not weaker than the passkey itself — additional registered passkeys across devices (synced passkeys largely solve this), plus one-time recovery codes stored offline. Critically, recovery must not fall back to an SMS reset that becomes the account's true weakest link, which would negate the phishing resistance.

Practice: Diagnose A Phishing Success

Task

Explain why TOTP was phished and whether/why WebAuthn would have prevented it.

Show solution

TOTP failed because it is a shared secret the user can read and retype, which makes it relayable. The fake page presented a convincing login, the employee typed their password and read their authenticator's six-digit code into the form, and the attacker's server immediately replayed both to the genuine site. TOTP proves the user has the seed, but it does nothing to prove which site the user is talking to — so a human-in-the-middle page harvests and forwards the code inside its ~30-second validity. 2FA "worked" exactly as designed; the design just does not resist real-time relay phishing. SMS codes fail identically.

WebAuthn/passkeys would have prevented it, structurally:

  • The credential is bound to the real site's origin. On the phishing domain, the browser finds no credential to offer for that origin, so there is nothing for the employee to hand over — the attack has no input to collect.
  • The signature covers the origin, so even if the attacker relayed the ceremony, the signed assertion is tied to the phishing origin and the real site rejects it.

There is no code to read aloud, no secret to retype, and no way for the user to be tricked into using their credential on the wrong site, because the protocol — not the user's vigilance — enforces the origin match. That is the difference between phishing-resistant (WebAuthn) and phishing-vulnerable-but-2FA (TOTP/SMS). The remediation is to move staff, especially privileged accounts, to passkeys or hardware keys, and ensure recovery does not reopen a phishable path.

Practice: Define WebAuthn Review Checks
Show solution
  • Tests assert each ceremony uses a fresh random challenge that is single-use and bound to the session, and that a replayed or stale challenge is rejected.
  • A test confirms only public keys and credential IDs are stored — never anything secret — and that one user can hold multiple credentials.
  • A test asserts the relying-party ID / origin configuration matches the real domain, since a misconfigured origin breaks the phishing resistance that is the whole point.
  • A test confirms the library's verification result is actually enforced (the response is checked, not just received).

Human, periodically:

  • Trace the recovery path and confirm it is not weaker than the passkey — specifically that account recovery does not silently fall back to SMS or an emailed link that becomes the real weakest link. This is the most common way a passkey rollout is quietly undermined.
  • Confirm high-value account types are steered to or required to use passkeys/hardware keys rather than leaving phishable factors as their protection.
  • Review credential inventory per user for anomalies (unexpected new registrations), tying into application-compromise monitoring.