Blog · Security

Why passkeys stop phishing (and one-time codes don't)

Phishing-resistant authentication explained: how origin binding lets passkeys defeat adversary-in-the-middle kits that steal one-time codes and push approvals.

Passkeys stop phishing because the browser, not the user, decides which website a credential is used for. Every passkey is bound to the domain it was created on, and the signature it produces includes the origin of the page that asked for it. A phishing site on a look-alike domain can’t get the browser to use your passkey at all, and if it relays a request to the real site, the origin in the signed response gives it away. One-time codes have no such binding: a code typed into a fake page works just as well when the attacker replays it on the real one.

That difference is why passkeys count as phishing-resistant authentication and SMS codes, authenticator-app codes and push approvals don’t.

How modern phishing actually works

The fake login page that simply collects a password is still around, but the serious threat today is the adversary-in-the-middle (AitM) phishing kit. These kits run a reverse proxy between the victim and the real site:

  1. The victim clicks a link to login.examp1e-secure.com.
  2. The proxy fetches the real sign-in page from login.example.com and serves it, pixel-perfect, under the attacker’s domain.
  3. The victim types their username and password. The proxy forwards them to the real site.
  4. The real site asks for a one-time code. The proxy shows that prompt too.
  5. The victim types the code. The proxy forwards it. The real site accepts it, because it’s a valid code.
  6. The real site sets a session cookie. The proxy captures it and hands it to the attacker.

The attacker now has a signed-in session. They didn’t need to break any cryptography. They only needed the user to type secrets into the wrong page.

Ready-made kits automate all of this, so it isn’t a technique reserved for sophisticated attackers.

Why one-time codes fail

Every code-based factor has the same flaw: the code is a bearer secret that isn’t tied to where it was entered.

FactorCan an AitM proxy relay it?
PasswordYes
SMS codeYes
Authenticator app (TOTP) codeYes
Push “Approve this sign-in?”Yes, the user approves the attacker’s session
Email magic linkOften, if the user is tricked into pasting or forwarding it, or the inbox is compromised
Passkey (WebAuthn)No

Short expiry times don’t help. A proxy relays the code in well under a second. Number matching on push prompts makes blind approval harder, but a user looking at a convincing proxied page will still type the number they see.

Origin binding: the property that matters

Passkeys are different because of two checks built into WebAuthn.

1. The browser enforces the relying party ID

When a passkey is created, it’s scoped to a relying party ID, such as example.com. When a page calls navigator.credentials.get(), the browser only allows an RP ID that matches the page’s own domain (or a registrable parent of it). A page on examp1e-secure.com can’t ask for credentials for example.com. The user’s device simply has no passkey to offer.

There’s nothing for the user to get wrong. They don’t need to inspect the URL, because the browser already did.

2. The signature covers the origin

Even if an attacker tries to relay a WebAuthn challenge from the real site through their proxy, the browser builds clientDataJSON using the actual origin of the page the user is on, and the authenticator signs a hash of it. The real server checks that origin:

const clientData = JSON.parse(
  new TextDecoder().decode(assertion.response.clientDataJSON)
);

if (clientData.type !== "webauthn.get") throw new Error("wrong ceremony");
if (clientData.origin !== "https://login.example.com") {
  throw new Error("origin mismatch: possible phishing proxy");
}
// ...then verify the challenge, RP ID hash, flags and signature

The attacker can’t edit the origin without invalidating the signature, because they don’t have the private key.

3. Cross-device sign-in checks proximity

When a user signs in on a computer with a passkey on their phone, the phone scans a QR code and then confirms over Bluetooth that it’s physically near the computer. A remote attacker can’t simply send a victim a QR code from a distant machine and have the phone sign for it.

What passkeys don’t fix

Phishing resistance applies to the sign-in ceremony. A few things still need attention:

  • Fallback methods. If a user can also sign in with a password or SMS code, the proxy kit will target that. Passkey-only sign-in is what closes the gap. See passkeys vs passwords, SMS codes and magic links.
  • Account recovery. Recovery is the new weakest link. It should prove control of something meaningful, expire quickly and revoke old credentials. More in passkey account recovery.
  • Session theft after sign-in. Malware on the device can steal cookies regardless of how the user authenticated. Short-lived tokens and HttpOnly cookies limit the damage.
  • Consent phishing. Tricking a user into granting a malicious app OAuth access is a different attack, handled by careful client registration and redirect URI validation.

Phishing-resistant authentication checklist

If you’re evaluating whether your sign-in is phishing-resistant:

  1. Everyday sign-in uses WebAuthn passkeys or security keys.
  2. No password, SMS or TOTP path can complete a sign-in on its own.
  3. The server verifies origin, RP ID, challenge and user verification on every assertion.
  4. Recovery is single use, short-lived and revokes existing credentials.
  5. Sessions use HttpOnly, Secure cookies, and tokens are short-lived.

How Gatekeep applies this

Gatekeep is built around that checklist. Everyday sign-in is passkey-only, with no passwords, SMS or authenticator codes to relay. Email links are used only for signup and recovery; they expire in 10 minutes, work once, and completing recovery revokes every passkey on the account. Session cookies are HttpOnly, Secure and SameSite=Lax, and access and ID tokens last 15 minutes.

Because passkeys are bound to the domain they’re created on, running sign-in on your own domain from day one matters. Read the security model for the details, and request early access.

Delete the password field.

Passkey sign-in for every app you ship. Free up to 5,000 monthly active users.