Threat Model
Published
An API key and an access token are different objects with different lifetimes and different controls, and advice written for one fits the other badly. A key should never have been in the bundle at all. A token legitimately arrives in the browser, which changes the question from where to hide it to what bounds it once it is there.
A key and a token are not the same object
A static API key identifies your application. It is the same for every user, it lives until somebody rotates it, and if it grants anything worth having then shipping it to browsers hands that capability to everyone. The advice for those is unambiguous and the API key article gives it in full: the key belongs on a server, and no amount of processing changes what a value in a downloaded file is.
An access token is a different animal. It is issued to one user after they authenticate, it is scoped to what that user may do, it expires on a short timer, and it can be revoked. It is in the browser because the browser is the thing making the requests, and putting it there is the design working as intended rather than a leak.
That difference reframes the question entirely. Nobody sensible asks how to hide a session from the session's owner. What they should be asking is how long the token is useful for, what it is scoped to, whether it can be detected once stolen, and whether it is worth anything to somebody holding it somewhere else. None of those four are answered by how legible the surrounding code is.
What protection changes and what it does not
Start with what does not move. The token is in memory while your application runs, it is usually in a storage API between page loads, and it is on the wire in a header on every request you make. Anything executing on your origin can read all three. Protection acts on code rather than on data, which is the general point the browser storage article makes and which applies to tokens exactly.
There is a narrower thing that does move. Automated credential-harvesting works at scale by recognising patterns: a known storage key, a recognisable variable name, a familiar shape of refresh call. Code that has been through a transform is measurably worse input for that kind of generic sweep, because the recognisable names are gone and the structure it pattern-matched against has changed. That is a real benefit against untargeted tooling and it decays to nothing the moment somebody spends an afternoon on your specific application.
The asymmetry underneath is the part that decides how much to invest. The attacker who takes your users' tokens at scale is usually running code on your origin already, through an injection or a compromised dependency. Code running on your origin does not have to read your bundle at all. It can call the same functions your application calls, read the same storage your application reads, and observe the same responses, because from the browser's perspective it is your application. The malicious dependency article works that through, and it is why token protection is an architecture question rather than a build question.
The three controls that actually bound a token
A short lifetime. The only control that works without any cooperation from the client, because it is enforced where the token is validated. A token good for minutes limits the value of theft to minutes. This is unglamorous and it is the single highest-return thing on the list, which is why the entire pattern of short access tokens plus a longer-lived refresh credential exists.
Rotation with reuse detection. Issue a refresh credential that is valid exactly once, and invalidate it when it is exchanged. If a consumed one is ever presented again, either the legitimate client or the thief is using a copy, and the correct response is to revoke the whole family and force re-authentication. This is the control that converts silent theft into a detected event, which matters because the failure mode you should fear is the one you never learn about. Nothing in a build step contributes to it.
Sender-constraining. A bearer token works for whoever holds it, which is the whole problem in one sentence. A sender-constrained token is bound to a key the legitimate client holds, and each request carries a proof signed with that key, so possession of the token alone is not sufficient. The common browser shape generates a key pair with the platform crypto API, keeps the private key non-extractable, and signs a small proof per request.
Notice what those three have in common. Each is enforced by the party validating the request, and each changes what a stolen token is worth. A build setting changes how hard the token is to locate, which is a different axis and a shallower one.
Non-extractable does not mean unusable
The sender-constraining pattern deserves a precise statement, because it is frequently oversold and the honest version is still good. A key pair generated as non-extractable gives you a handle rather than key material. Script can ask the platform to sign with it and cannot ask for the bytes, so the private key does not leave the browser even if the storage holding the handle is read wholesale.
What that stops is exfiltration and replay elsewhere. An attacker who copies your storage now holds a token and a reference to a key they cannot take with them, so the stolen material is not enough to make a valid request from their own machine.
What it does not stop is abuse in place. Script running on your origin can use the handle to sign, because that is precisely what the legitimate application does and the platform cannot tell the two apart. So an injection on your page can still make authenticated requests for as long as it is running there.
That is a genuine and worthwhile reduction, not a defeated attack. It converts a portable credential into one tied to continued presence on your origin, and continued presence is far easier to notice and cut off than a token quietly working from somewhere else. It is also entirely independent of your build configuration, which is worth saying because the two are often bundled together in vendor copy as though one delivered the other.
Where the build step legitimately helps
It raises the cost of the broad, untargeted case. Generic tooling that sweeps many sites for recognisable credential-handling patterns does worse against transformed code, and that population of attacker is real and large. Treat it as a cost multiplier on one attack shape rather than as a control over the credential.
It gives you somewhere to attach signals. Runtime guards are more useful here as sources of evidence than as barriers: knowing that an integrity check tripped on a session is information your backend can act on, and acting on it means revoking, which is a real control. The runtime defence options describe what each guard reports and what its failure action does.
It buys time on the parts of your client that are genuinely proprietary, which is what the transform is for. Token handling is rarely one of those parts, because it is usually a standard flow implemented the standard way, and the standard way is documented publicly anyway.
What it does not do is extend a token's lifetime, scope it more tightly, make its theft detectable, or bind it to a client. Those four are where the security of a token comes from, and all four are decisions on the server that issues and validates it.
The short version
Do not carry advice about API keys over to access tokens. A key in your bundle is a design error; a token in your browser is the design. Once it is there, the questions that matter are how long it lives, what it is scoped to, whether reuse of a rotated credential is detected, and whether it is worth anything to somebody holding it off your origin.
Protection makes token-handling code a poorer target for generic automated harvesting, which is worth having and is not the control. Short lifetimes, rotation with reuse detection and sender-constraining are the controls, they are enforced by the server, and none of them depends on how readable your client is.
Frequently asked questions
Does obfuscating my JavaScript protect access tokens?
Not in the way the question implies. The token is in memory while your application runs, usually in a storage API between loads, and in a request header on the wire, and anything executing on your origin can read all three regardless of how the surrounding code was built. What protection does change is the cost of generic automated harvesting, which works by recognising familiar storage keys and code shapes across many sites at once. That is a real but narrow benefit against untargeted tooling, and it is not what bounds the credential.
Is an access token the same problem as an API key in my bundle?
No, and treating them the same leads to the wrong work. A static API key identifies your application, is identical for every user and lives until rotated, so shipping one to browsers hands that capability to everybody and the fix is to move it to a server. An access token is issued to one user, scoped, short-lived and revocable, and it is in the browser because the browser makes the requests. The token belongs there, so the useful questions are about its lifetime, scope and detectability rather than about where to hide it.
What actually limits the damage if a token is stolen?
Three things, all enforced where the token is validated. A short lifetime caps the value of theft to the remaining window. Rotation with reuse detection makes a refresh credential valid exactly once, so presenting a consumed one signals that a copy exists and lets you revoke the whole family. Sender-constraining binds the token to a key the client holds so that possession alone is insufficient. None of the three depends on the readability of your client code, and all three are decisions on the server.
Does storing a key as non-extractable stop token theft?
It stops one specific and important part of it. A non-extractable key gives script a handle rather than the key bytes, so an attacker who copies your storage cannot take the private key with them and cannot make valid requests from their own machine. It does not stop abuse in place: script running on your origin can still ask the platform to sign, because that is exactly what the legitimate application does and the platform cannot distinguish them. The gain is converting a portable credential into one tied to continued presence on your origin, which is much easier to detect and cut off.
If an attacker can run script on my origin, does any of this help?
Less than most defences imply, and it is better to plan around that honestly. Script on your origin does not need to read your bundle, because it can call the functions your application calls and read the storage your application reads. Against that attacker the useful controls are the ones that limit what a session can do and how long it lasts, plus the ability to revoke on a signal. The durable work is preventing that script from running at all, which is a content security policy and dependency review question rather than a build setting.
Where does obfuscation legitimately fit into token handling?
As a cost multiplier on untargeted attacks and as a place to attach signals, layered under the controls that do the bounding. Transformed code is poorer input for tooling that sweeps many sites looking for recognisable credential patterns. Runtime guards are more valuable here as evidence sources than as barriers, because a tripped check reported to your backend can trigger revocation, and revocation is a real control. What the build step does not do is change a token's lifetime, scope, detectability or portability.
Related reading