Where Authority Lives

Your JavaScript cannot authenticate a payment

Payment authentication rules pushed a great deal of new work into checkout flows, and a recurring question followed it here: if our authentication logic is in the bundle, should we protect it? The answer is more useful than yes or no, because the premise usually points at a design problem. If any part of authenticating the payer is happening in your JavaScript, the thing to fix is not how that code was compiled.

Authentication is a claim about who checked

Strong customer authentication means the payer was verified using factors from independent categories, and that verification is performed by whoever holds the credentials being checked. For a card payment that is the issuer. For a bank transfer it is the bank. It is not the shop, and it is definitely not the shop's front end.

Your page has a real job in that flow, and it is a courier's job. It collects what the payer entered, hands control to a context the issuer controls, waits, and carries a reference back. Every one of those steps is necessary. None of them is the verification, and none of them can substitute for it.

Once that is clear, the protection question mostly dissolves. There is no authentication secret in your bundle to protect, because the authentication never happened there.

Dynamic linking, and why a browser cannot produce it

The requirement that makes this concrete is dynamic linking: the authentication has to be bound to the particular amount and the particular payee, so that approving one payment does not approve a different one.

That binding has to be created where the amount is authoritative. A browser can display an amount beautifully and is still not the source of truth for it, because the code that renders the number is code the payer can modify. A page that computes what to authenticate and asks the issuer to bind to that value has built a system where the payer chooses what they are approving.

So the amount and payee travel from your server through your provider to the issuer, and the payer confirms them in an interface your page does not own. This is the same argument this site makes about prices computed in the browser, arriving at a place where the consequences are handled by a regulated third party rather than by your reconciliation.

The failure that actually happens

Almost nobody sets out to authenticate in the browser. The real defect is quieter: a page reads a success signal from a callback and treats it as the fact of payment.

It is easy to see how. The flow returns, the SDK fires an event with a status, and there in your handler is a value that says everything went well. Hanging fulfilment off it makes the interface feel immediate, and it works perfectly in every test you run, because in testing nobody is lying to you.

What that design has done is make the browser the authorisation system. Any client-reported state is a claim by a party with an interest in the outcome, on hardware they control, and this is the highest-value claim on the page. The correct sequence is that your provider learns the outcome, your server confirms it with the provider directly, and your server decides what happens next. Reacting in the page to make the experience responsive is fine, as long as nothing irreversible depends on it.

This is the same rule as every other client-reported fact, applied where getting it wrong involves money and a chargeback rather than a cosmetic glitch.

Exemption logic in the client scores its own subject

The friction that authentication adds creates real pressure to avoid it where the rules allow, and that pressure produces the second mistake: deciding in the browser whether a transaction qualifies for an exemption.

Set aside who is permitted to make that decision, which is a question for your acquirer and your provider rather than for us. Consider only the engineering. Such a decision is computed from inputs the payer supplies, on a device the payer owns, using rules the payer can read and change. It is a fraud assessment whose subject is also its operator.

And unlike most client-side logic, this one leaks something useful when read. Thresholds, trusted-device conditions, velocity checks and value bands describe the boundaries of your fraud controls, which tells somebody probing them exactly where the edges are. Protecting that code raises the cost of reading it. It does not change the fact that the reasoning is happening in the wrong place, and the boundaries can be mapped from the outside by varying the transaction anyway.

Request exemptions from server-side context through your provider, and let the party carrying the liability decide.

Your integration is legible however you build it

A fair question at this point is whether the checkout bundle is worth protecting at all, given none of the above is helped by it.

Be clear-eyed about what is visible regardless. Which provider you use, which endpoints you call, the shape of your requests, which fields you validate, where the flow branches on a response — all of it is in the network panel, because the requests must be made and the responses must be read. Transformation makes the code that builds those requests harder to follow. The traffic is unchanged.

What protection is genuinely doing on a checkout page is raising the cost of lifting your implementation and of casually tampering with client-side state. Both are legitimate once the consequential decisions are elsewhere. Neither is the control, and a checkout whose integrity depends on the bundle being hard to read is a checkout with a design problem underneath it.

The risk that is actually on your payment page

Meanwhile, the thing most likely to cost you money on that page is not your code at all. It is everything else loaded next to it.

Analytics, tag managers, chat widgets, session recording, personalisation, an experiment framework: each one executes with the same access to your form as your own script. A third-party script that is compromised, swapped or simply updated by its vendor can read fields as they are typed, and that mechanism is behind most card-skimming incidents rather than anything exotic. Protecting your own bundle does nothing about it, because the attacker is not modifying your bundle.

The disciplines that address it are inventory and change detection: know exactly which scripts are permitted on that page, and notice when the set or their content changes. That is what the payment-page requirements in the card industry standard are pointing at, and this site covers them on the PCI DSS page and in the article on tag managers. Reducing the script surface on a checkout page is one of the highest-value hours available to a front-end team, and it is unglamorous enough that it rarely gets scheduled.

What a sound arrangement looks like

The server owns the amount and the payee. The provider and the issuer own the authentication. The page renders what is happening and carries references, deciding nothing consequential. Outcomes are confirmed server to server before anything is released, with the client callback used only to update the interface. Exemptions are requested from the server by the party permitted to request them. Third-party scripts on the payment page are enumerated, minimised and monitored.

On top of a design like that, protecting the checkout bundle is a reasonable hardening measure that costs little and raises the cost of casual interference. Underneath a design that is missing those properties, it is decoration over the actual problem.

Three checks worth running

Find every place your code concludes that a payment or an authentication succeeded, and follow each back to its source. If any path ends at a client callback rather than a server-side confirmation, that is the first fix.

Search the checkout bundle for the vocabulary of decisions rather than for a module name: score, risk, threshold, eligible, exempt, trusted, velocity, limit. For each hit, ask whether it decides anything or merely formats something the server already decided.

List every third-party script that loads on the payment page and name the person who would notice if one changed. In most audits the first two come back clean or take an afternoon to fix, and the third is where the genuine exposure turns out to be.

The short version

Authentication is a claim about who verified the payer, and your page is not in a position to make it. Dynamic linking has to be created where the amount is authoritative, which is not a browser. Never treat a client callback as the fact of payment, and never let the client decide an exemption about itself. Your integration is readable in the network panel however the bundle was built, so keep everything consequential server-side — and spend the effort you were going to spend on protecting checkout logic on knowing which third-party scripts run beside it instead.

Frequently asked questions

Can we implement strong customer authentication in our front end?

No, and the reason is structural rather than a matter of doing it carefully. Strong customer authentication is a statement about who verified the payer, and the parties who can make that statement are the ones holding the credential being verified, which means the card issuer or the bank rather than your page. Your JavaScript can present a challenge, hand control to the issuer's interface, and carry a result back. It cannot perform the verification, and it cannot vouch for one that happened. Any design where the browser decides that authentication succeeded has put the decision on the one machine in the transaction that the payer fully controls.

What does dynamic linking mean for the client side?

It means the authentication has to be bound to the specific amount and the specific payee, and that binding has to be created where those values are authoritative. The point of the requirement is that a payer who approves forty euros to one merchant has not approved four hundred to another, so the code the payer approves is tied to those details. A browser can display an amount, but it is not the source of truth for one, because the same page that renders the number can be made to render a different one. The binding therefore belongs to the issuer and your server, and the client's role is to show the payer what is happening.

Our provider gives us a JavaScript SDK. Does that change the answer?

It changes who writes the client code, not where the authority sits. Those SDKs are careful precisely because of everything above: they hand off to an issuer-controlled context, and the result that matters comes back to your server from the provider rather than from the page. The mistake to avoid is reading a success signal in the browser and treating it as the fact of payment. Confirm the outcome server to server before you release goods or grant access, and treat whatever the client reports as a hint about what to render next. That is the same rule as every other client-reported state on this site.

Can we decide exemptions in the client to reduce checkout friction?

No, and this is the version of the mistake that costs money rather than merely being untidy. Exemptions are decisions taken by the party that carries the liability when they turn out to be wrong, which is your acquirer or the issuer depending on the type. A risk assessment computed in the browser is computed from inputs the payer supplies, on a machine the payer controls, using logic the payer can read and edit. Even leaving the rules aside, it is a fraud-scoring system that lets the subject choose their own score. Request an exemption through your provider from server-side context, and let the party who bears the outcome decide.

Does obfuscating our checkout page help with any of this?

Not with the authentication question, which is settled by architecture rather than by how the code was written. It helps with a narrower and genuinely real problem: raising the cost of reading and casually tampering with your checkout logic, which is worth having once the important decisions are somewhere the customer is not. The far more valuable work on a payment page is knowing exactly which scripts run on it and detecting when that set changes, which is a different discipline and the subject of the payment-page guidance on this site.

What can an attacker learn from our checkout bundle anyway?

The shape of your integration, which is useful reconnaissance and not a secret you can keep. Which provider you use, which endpoints you call, what your request bodies look like, which fields you validate and how, and where your flow branches. All of that is visible in the network panel regardless of how the bundle was built, because the requests have to be made and the responses have to be read. Protection makes the code that constructs them harder to read; it does not make the traffic private. Design on the assumption that your integration is legible, and keep everything consequential behind a server that re-verifies.

Where do the real client-side risks on a payment page sit?

In what else is running on the page, rather than in your own code. A payment page typically loads analytics, tag managers, chat widgets and other third-party scripts, any of which executes with the same access to the form as your own JavaScript does. A compromised or swapped third-party script is the mechanism behind most card-skimming incidents, and no amount of protection applied to your own bundle addresses it. Reduce the script surface on that page to what genuinely has to be there, and monitor the set for change. That is where the effort pays.

How should we handle the result coming back from the challenge?

As a signal to update the interface, never as the fact of the transaction. The reliable sequence is that the payer completes the challenge in the issuer's context, your provider learns the outcome, your server confirms it with the provider directly, and your server then decides what happens. The page can react to a client-side callback immediately so the experience feels responsive, and that is fine as long as nothing irreversible hangs off it. If your fulfilment is triggered by a browser telling you it went well, then the browser is your authorisation system.

What should we actually check in our own implementation this week?

Three things, all quick. Find every place your code concludes that a payment or an authentication succeeded, and confirm each one traces back to a server-side confirmation rather than to a client callback. Search your checkout bundle for risk and eligibility vocabulary, anything scoring, thresholding or deciding, and ask whether that decision has any business running in a browser. Then list every third-party script that loads on your payment page and remove the ones nobody can justify. The first two are usually clean or quickly fixed. The third is where most teams find something surprising.

Related reading