Phishing-Resistant Auth · Chapter 2 of 5
Origin binding: the core idea behind passkeys
Replace a typed secret with a signature over the origin the browser actually sees. Relay becomes useless by construction.
The previous chapter located the flaw precisely: the user is the component that decides where the secret goes, and users can be deceived about origins. Origin binding removes the user from that decision. Instead of producing a secret the user carries to a website, the authenticator produces a signature that includes the website’s identity as reported by the browser, and the website checks that identity before accepting the signature.
Three ingredients
Origin binding needs three things working together. First, a per-site key pair: the authenticator creates a fresh private key for each relying party and never uses it anywhere else. Second, a scoped lookup: when asked to sign, the authenticator only offers keys whose stored relying party identifier matches the current site. Third, a browser-attested origin: the signed data includes the origin taken from the browser’s own address bar state, not anything the page can choose.
Each ingredient blocks a different version of the attack. Per-site keys mean a signature for one site is meaningless for another. Scoped lookup means that on a phishing domain the authenticator has no key to offer at all, so the user simply sees no passkey for that site. The browser-attested origin means that even a confused or compromised authenticator cannot produce a signature that the real site would mistake for one created on its own pages.
Why the browser is trusted
The browser already enforces the same-origin policy, validates TLS certificates and decides which cookies a page may read. It is the component that knows, without ambiguity, that a page came from https://larkspur-ledger-secure.example rather than https://larkspur.example. Origin binding reuses that knowledge. JavaScript on the page can call the authentication API, but it cannot lie about its own origin, because the browser fills that field itself.
This is also the boundary of the guarantee. If the attacker controls the browser — malware with the ability to drive it, or a malicious extension with broad permissions — origin binding can be bypassed, because the attacker can make legitimate requests from the real origin. Phishing resistance is a defence against remote deception, not against a compromised endpoint. That is still an enormous improvement, because remote deception is what phishing kits are.
Public keys change the breach story
Origin binding arrives together with asymmetric cryptography. The server stores only a public key per credential. Contrast this with series 1, where a leaked seed table let an attacker mint codes for everyone. A leaked table of WebAuthn public keys lets an attacker do nothing, because verifying a signature is not the same as producing one. This is the second reason, alongside phishing, that passkeys are a structural upgrade rather than an incremental one.
- OTP seeds: symmetric, server-readable, leak = mint codes.
- Passwords: hashed, leak = offline cracking.
- WebAuthn public keys: leak = nothing usable for authentication.
The relying party identifier
WebAuthn calls the website the relying party and identifies it with an RP ID, which is a registrable domain suffix of the page’s origin. A page at https://app.larkspur.example may use RP ID larkspur.example or app.larkspur.example, but not larkspur-ledger-secure.example and not example. The authenticator stores the RP ID with each key, and the server checks that the signed data contains the SHA-256 hash of the RP ID it expects.
Choose the RP ID deliberately. Using the apex domain lets the same passkey work across subdomains such as app. and accounts.; using a specific subdomain isolates it. Changing the RP ID later orphans every existing credential, so treat it as a permanent identifier. If you must serve several distinct domains with one credential set, the platform-level related-origins mechanism exists, but it adds complexity that most teams do not need.
// Server-side checks that make the binding real (pseudocode)
const client = JSON.parse(utf8(clientDataJSON));
assert(client.type === "webauthn.get");
assert(client.origin === "https://larkspur.example"); // exact match
assert(timingSafeEqual(b64url(client.challenge), session.challenge));
assert(equal(authData.rpIdHash, sha256("larkspur.example")));
assert(verifySig(storedPublicKey, authData || sha256(clientDataJSON), sig));The next chapter walks the full WebAuthn ceremony — registration and authentication — including the parts this chapter skipped: user presence and verification flags, signature counters and attestation. The core idea, though, is already complete. Bind the response to the origin the browser sees, keep a separate key per site, and the relay attack from chapter 1 has nothing to relay.
It helps to compare this with how a typed code fails. An OTP has no idea where it is going. It is a short string that is equally valid on any page, so its security depends entirely on the user picking the right page. A WebAuthn assertion is the opposite: it is useless anywhere other than the origin it names, and the user never sees the bytes, so they cannot be talked into sending them somewhere else. The human still approves the action with a touch or a fingerprint, but the human no longer routes the secret.
There is a cost to this. Origin binding only works when the browser can see the origin, so it requires authentication to happen in a real browser context, not inside an embedded web view that hides the URL or in a native app that skips the platform authentication APIs. Native apps link themselves to a web domain using platform association files so their passkeys share the website's RP ID. Plan for every surface where users sign in, including support tools and partner portals, before choosing the RP ID.
// bench check
0/3 correct · 0 answered
1. On a phishing domain, what usually happens when the page calls the passkey API?
2. Who supplies the origin inside clientDataJSON?
3. Why is a leaked table of WebAuthn public keys low impact?