Authentication
Published
Passkeys change the honest answer to a question this site gets often. When the credential is a key held by the authenticator and never exposed to the page, what exactly is left for client-side protection to do in a login flow? The answer is real but narrower than most people assume — and it is not the part of the code anyone expects.
The credential never touches your JavaScript
Start with the mechanism, because it settles most of the question. In a WebAuthn ceremony your page calls navigator.credentials.create or navigator.credentials.get with a challenge your server issued. From that point the browser takes over: it shows its own UI, talks to the authenticator, and the private key stays inside the authenticator hardware or the platform keystore. Your JavaScript receives a signed assertion and forwards it.
So there is no secret in your login code to protect. There is no key material, no shared secret, no token derivation worth reading. This is a genuine architectural improvement over password and API-key flows, and it is worth naming because so much protection advice is written for the world where the client did hold something sensitive — the world described in you cannot hide an API key in JavaScript.
Obfuscating a passkey ceremony therefore protects no credential. Anyone who wants to know how it works can read the specification.
What passkeys give you that obfuscation cannot
Passkeys are origin-bound. The browser scopes each credential to a relying-party identifier and refuses to release an assertion to a different origin. That single property defeats the entire class of credential phishing that no amount of client-side hardening has ever addressed: a convincing replica on a lookalike domain simply cannot obtain a usable assertion, because the browser will not produce one.
It is worth sitting with the contrast. Obfuscation raises the cost of understanding code. Origin binding makes an attack cryptographically impossible. Where a real control of the second kind is available, use it and do not spend effort trying to approximate it with the first.
The same reasoning applies to replay. The challenge is server-issued and single-use, and the signature covers it, so a captured assertion is not reusable. Again: a property of the protocol, not of how readable your code is.
What is worth protecting in an authentication bundle
Which is not to say the code around the ceremony is uninteresting. Three things usually are.
Risk and device heuristics. Most real deployments do not treat every authentication identically. They score the session — device familiarity, behavioural signals, velocity, network reputation — and use the score to decide whether to step up. That logic ships to the client when it runs there, and reading it tells an attacker exactly which signals to normalise. This is the strongest case for protection in an auth flow, and it is the same argument as the assessment platform protecting its integrity heuristics rather than its answer key.
Fallback and recovery paths. Passkey rollouts almost always retain a fallback, and the fallback is where the weakness lives: emailed magic links, SMS codes, security questions, an account-recovery flow with different rules. The recovery path is what a competent attacker attacks, and its client-side logic reveals its shape. Protect it, and more importantly design it as carefully as the primary path.
Enrollment and policy logic. Which authenticators you accept, how attestation is evaluated, when a second credential is required. Worth making expensive to read, for the same reason as the risk rules.
The mistake that obfuscation cannot rescue
There is one design error worth naming explicitly, because it recurs and because protection is often reached for as the mitigation.
Verification must happen on the server. The server generates the challenge, stores it against the session, receives the assertion, verifies the signature against the stored public key, checks the origin and the relying-party hash, and validates the signature counter. If any of that is done in JavaScript — or if the client is trusted when it reports that verification succeeded — then authentication is a value the browser chose, and an attacker skips your code entirely and posts the result your endpoint expects.
Obfuscation makes that code harder to read. It does not make the answer trustworthy, because the attacker does not need to read it. This is the same boundary that governs exam scoring, paywall entitlement and license checks: if the client computes it, the client can choose it.
A false positive worth pre-empting
One practical interaction, which is easy to fix and unpleasant to debug in production.
Passkey providers and password managers commonly hook the credential and form APIs to offer their own UI. The default anti-tamper watch list does not include navigator.credentials, so that specific hook will not trip the guard. But those same extensions almost always also wrap EventTarget.prototype.addEventListener to observe form interaction, and often fetch or XMLHttpRequest — all of which are watched by default.
So the tamper signal fires during sign-in, for users running exactly the credential-management software you want them to use. Exclude the relevant paths with AntiMonkeyPatchingExcludeGlobals, and keep the failure action at callback or degrade. Blanking the page at the authentication step, for the population most likely to be using a password manager, is a support burden with no security benefit — the same proportionality argument as browser extensions generally.
The useful shape is to feed the tamper signal into the server-side risk decision. A patched environment is a reasonable input to whether this session may change an email address or move money. It is a poor reason to refuse to render a login form.
The short version
Passkeys remove the secret from your client code, so obfuscating a WebAuthn ceremony protects nothing about the credential, and the phishing resistance you gain comes from origin binding rather than from anything you can do to your JavaScript. Protect the code that is genuinely yours: risk scoring, device heuristics, enrollment policy and above all the fallback and recovery flows where the real weakness lives. Verify every assertion on the server, because a client-side verdict is one an attacker can simply assert. And expect password managers to trip your tamper guard during sign-in, so exclude the globals they wrap and respond by adjusting trust rather than by breaking the page.
Frequently asked questions
Does obfuscating my login code make passkeys more secure?
No, because there is nothing sensitive in that code to conceal. The private key stays inside the authenticator or platform keystore and never enters JavaScript. Your page calls navigator.credentials with a server-issued challenge, the browser runs its own UI and talks to the authenticator, and your code receives a signed assertion to forward. There is no key material, shared secret or token derivation in the ceremony, and the protocol itself is public.
What in an authentication flow is actually worth protecting?
Three things. Risk and device heuristics, because reading them tells an attacker exactly which signals to normalise to avoid a step-up challenge. Fallback and recovery paths, since magic links, SMS codes and account recovery are where a competent attacker goes and their client-side logic reveals the shape of those flows. Enrollment and policy logic, meaning which authenticators you accept, how attestation is evaluated and when a second credential is required.
Can obfuscation prevent phishing of a login flow?
No, and passkeys already solve that problem properly. Credentials are bound to the relying-party origin, and the browser refuses to release an assertion to a different one, so a replica on a lookalike domain cannot obtain a usable credential no matter how convincing it is. That is a cryptographic guarantee rather than a cost increase. Where a control of that kind exists, use it rather than trying to approximate it with client-side hardening.
Can WebAuthn assertions be verified in the browser?
No, and treating a client-reported result as authoritative is the one design error obfuscation cannot rescue. The server must issue the challenge, store it against the session, verify the signature against the registered public key, check the origin and relying-party hash, and validate the signature counter. If the client decides whether verification succeeded, an attacker skips your code and posts the result your endpoint expects, without needing to read anything.
Why does my tamper guard fire when users sign in with a password manager?
Because credential-management extensions wrap APIs the guard watches. The default watch list does not include navigator.credentials, so hooking that specific API is not the trigger. But those extensions also wrap EventTarget.prototype.addEventListener to observe form interaction, and frequently fetch or XMLHttpRequest, all of which are watched by default. Exclude the relevant dotted paths with AntiMonkeyPatchingExcludeGlobals rather than disabling the guard.
What should happen when a tamper signal fires during authentication?
Feed it into the server-side risk decision rather than into a rendering decision. A patched environment is reasonable input to whether this session may change an email address, add a payee or move money, and it belongs alongside your other risk signals. Blanking or throwing at the sign-in step punishes the population most likely to be running a password manager, which is behaviour you want to encourage, and produces support tickets nobody can diagnose.
Related reading