
Read the abstract of RFC 7636, the specification for PKCE, and you’ll find exactly one sentence about what the document does: “OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to the authorization code interception attack.” That’s the whole scope statement. No browsers. No JavaScript. No mention of where a token sits after it arrives. And yet plenty of developers who have shipped a login flow but never opened the spec will tell you, with real confidence, that adding PKCE to a single-page app is what makes their OAuth token storage secure. That belief is wrong, and figuring out exactly why is the fastest way to understand what token storage actually is: not an implementation detail you fill in after picking a grant type, but a separate design decision that OAuth’s own history spent over a decade avoiding.
This is the on-ramp. If you’ve wired up a login library, seen access_token and refresh_token come back from somewhere, and never quite worked out why your team’s security review keeps asking where that token actually lives — this is for you.
Two Tokens, One Place They Have to Sit
Start with the vocabulary, because it’s used loosely everywhere and precision here pays off later.
A grant is just the recipe your app follows to get a token — the sequence of redirects and requests OAuth defines for proving who the user is and what they’ve authorized. An access token is the credential your app sends with every API call afterward; whoever holds it can act as the user, for as long as the token is valid. A refresh token is a longer-lived credential used to get new access tokens without asking the user to log in again — more dangerous if stolen, because it’s usually valid for hours or days rather than minutes. A public client is any application that can’t keep a secret — a browser-based app or a native mobile app, as opposed to a confidential client, typically a server, that can hold a client_secret nobody else can read.
Every one of those tokens has to physically exist somewhere the moment your JavaScript needs it: in a variable, in localStorage, in a cookie, in memory inside a closure. That “somewhere” is what “token storage” means. It’s a separate question from which grant got you the token in the first place — and OAuth’s own thirteen-year history is largely the story of those two questions getting confused with each other.

The Grant That Put the Token in the Browser on Purpose
RFC 6749, the original OAuth 2.0 specification from October 2012, defines a grant type built specifically for browser apps: the Implicit Grant. Instead of the browser getting a short-lived code and trading it for a token at a separate endpoint, the authorization server hands the access token straight back in the redirect itself, riding along in the URL fragment. RFC 6749 §10.3 is candid about what that costs, in the same document that defines it: “the access token is transmitted in the URI fragment, which can expose it to unauthorized parties.”
That wasn’t an oversight. RFC 6749’s authors weighed the trade-off in plain language, telling implementers in §1.3.2 that “this convenience should be weighed against the security implications of using implicit grants” before recommending the Authorization Code grant whenever it’s available. The Implicit Grant existed because, in 2012, a JavaScript app genuinely couldn’t do much better: browsers had no reliable way to strip a value out of the URL without a page reload, and CORS — the mechanism that lets a script make a request to a different domain, like an authorization server’s token endpoint — wasn’t dependably available. Putting the token straight in the URL, and cleaning up the URL bar as best you could, was a real engineering answer to a real constraint.
RFC 10017, the OAuth working group’s 2026 best-practice document for browser apps, names both of those constraints explicitly in its own “Historic Note” when it explains why the advice later changed: “Modern browsers now have the Session History API… Additionally, CORS has widespread support and is often used by single-page applications for many purposes.” Read that carefully: the document doesn’t argue the browser got more dangerous. It argues the excuse evaporated. Once a script could quietly rewrite the URL and reliably call a token endpoint on another domain, there was no longer a reason to accept a token sitting exposed in browser history and referrer headers just to save one round trip.
RFC 6749 never uses the word “deprecated” anywhere in its text — it just asks implementers to weigh a trade-off. RFC 10017 §7.2, in 2026, is unconditional: “Browser-based clients MUST use the Authorization Code grant type and MUST NOT use the Implicit grant type to obtain access tokens.” Preserve that MUST exactly if you quote it — it’s a normative keyword, not emphasis. It’s tempting to read the gap between those two documents as OAuth’s collective opinion hardening steadily over fourteen years. Resist that: an equally unconditional prohibition already existed in an early browser-apps draft back in November 2018, and RFC 9700, the general OAuth Security Best Current Practice published in between, actually shipped softer, conditional wording than that 2018 draft had. What took most of a decade was formal publication as a Best Current Practice, not a slow build-up of conviction.

PKCE Closes One Door, Not the Building
So the industry moved browser apps toward the Authorization Code grant — the one that hands back a short-lived code instead of a token, which the app then trades for a token at a separate endpoint. That grant has its own interception problem, and it’s a different one than the Implicit Grant’s. This is where PKCE comes in, and it’s worth being exact about what it targets, because the confusion at the top of this article starts here.
PKCE — Proof Key for Code Exchange, pronounced “pixy” — was published in September 2015 to close a specific hole: an attacker intercepting an authorization code in transit and racing the legitimate app to trade it for a token. Its own introduction describes the attack it was built for in concrete terms: interception “within a communication path not protected by Transport Layer Security (TLS), such as inter-application communication within the client’s operating system.” That’s a description of a native mobile app, not a browser — Figure 1 in the same section illustrates a malicious app on a smartphone registering the same custom URL scheme as the legitimate one, so the phone’s operating system hands the incoming authorization code to the wrong app. PKCE’s fix is to have the client generate a secret verifier, send only a derived challenge up front, and present the original verifier only at the final token exchange — so an attacker who intercepts the code in the middle still can’t complete the swap without a value they never saw.
Later guidance generalized PKCE to browser apps too, and for good reason: a browser is also a public client that can’t keep a client_secret, so the same binding trick helps there. But generalizing where PKCE gets used is not the same as expanding what PKCE does. It still binds one authorization code to the client instance that started that specific flow. It says nothing about where the token goes once the exchange finishes, because that was never the problem it was designed to solve. Nowhere in RFC 7636’s text does the phrase “cross-site scripting” or any variant of “malicious script” appear.

The Gap PKCE Was Never Asked to Close
Here’s the scenario that shows the gap directly. Imagine an attacker has already found a way to run their own JavaScript inside your application’s origin — through a cross-site-scripting bug, a compromised third-party script, whatever the entry point. RFC 10017 §5.1.3 walks through exactly what that attacker can then do, and one path doesn’t touch your stored tokens at all: “the attacker can inject a hidden iframe and launch a silent Authorization Code flow.” The hidden frame reuses the user’s still-active session with the authorization server, runs a completely ordinary, completely valid Authorization Code + PKCE exchange, and hands the attacker a brand-new, independently issued set of tokens — never touching anything your app already stored.
PKCE cannot stop this, and it isn’t a flaw in PKCE — it’s outside the problem PKCE was built to solve. The code-verifier binding that defeats an outside interceptor is irrelevant when the attacker’s code is the one starting the flow. RFC 10017 is blunt about the consequence: “There are no practical security mechanisms for frontend applications that counter this attack scenario.” Short-lived tokens don’t help, because the attacker’s tokens are fresh. Refresh-token rotation doesn’t help, because the attacker never touched the legitimate refresh token. Sender-constraining a token to a cryptographic key doesn’t help either, because the attacker can generate their own key pair for tokens they mint themselves.
One honest caveat belongs right next to that finding: RFC 10017 itself notes, in the same section, that pulling this off depends on “the authorization server supporting silent frame-based flows” — an authorization server that requires visible, interactive re-authentication on every session refresh closes this specific path, at the cost of the silent single-sign-on behavior most production systems rely on. This isn’t a loophole that makes the gap go away; it’s the RFC naming, precisely, the one condition the attack depends on.
This is the sentence worth sitting with: where you store an existing token and how new tokens get minted are two different attack surfaces, and PKCE was only ever standing guard over the second one’s front door — the code interception, not the token’s resting place.

So Where Does the Token Actually Go?
If PKCE doesn’t answer “where should the token live,” something still has to. The honest, sourced answer — worked out in full elsewhere in this collection, so only the headline belongs here — is that the usual browser storage options (localStorage, sessionStorage, IndexedDB, an in-memory variable) differ from each other only in exposure surface, not in whether a script running in your own origin can eventually reach them. RFC 10017’s own summary of its storage-mechanism survey: “none of these options can fully mitigate token exfiltration when the attacker can execute malicious code in the application’s execution environment.” Choosing the “safest-sounding” browser storage location is optimizing the wrong variable if the attacker you’re worried about can already run code where the token lives. OWASP’s own OAuth2 Cheat Sheet covers the implementation hygiene around this in more depth than fits here, if you want a second, vendor-neutral source.
Which raises the actual design question this whole arc has been circling: instead of picking a browser storage location more carefully, what if the token simply never reached the browser at all?
The Answer That Skips the Question
That’s the move RFC 10017 §6.1 formalizes as the Backend for Frontend pattern — a small server-side component, deployed alongside your frontend, that does something specific: “The BFF manages OAuth access and refresh tokens in the context of a cookie-based session, avoiding the direct exposure of any tokens to the browser-based application.” The browser gets an ordinary HttpOnly session cookie instead of a bearer token. The BFF holds the actual OAuth tokens server-side and attaches them to outgoing API calls itself. There is no localStorage key, no in-memory variable, no browser API to evaluate at all, because — in the RFC’s own words — “there are simply no tokens to be stolen.”
That’s a real, structural change, and it’s worth being precise about what it does and doesn’t buy you. It closes off the token-theft category entirely: nothing sits in browser-reachable storage waiting to be read, and the silent-reissuance trick from the previous section is also blocked, because the BFF, not the browser, is the confidential client running that exchange. What it does not do — and this hub covers the honest limits of that residual risk in detail elsewhere — is make an application immune to script running in its own origin generally. That’s a separate, harder problem than storage — one shared by any browser application that authenticates with a cookie at all, not something a BFF introduces or falls short on — and no architecture in this hub claims to solve it outright.
RFC 10017 §6.2 also describes a lighter middle position, the Token-Mediating Backend, where a server still runs the OAuth exchange but then hands the access token to the browser to use directly — a real intermediate step some teams land on and stay at, which is its own decision worth arguing through rather than assuming.

Storage Was the Decision All Along
Line up the three moves and the pattern is plain: the Implicit Grant chose to put the token in the browser deliberately, because the alternative was expensive. PKCE fixed a real, adjacent problem — authorization-code interception — without ever touching the storage question. The BFF is the first of the three that actually answers “where does the token live?” by removing the question: nowhere in the browser at all.
None of this required OAuth’s threat model to change in some dramatic way. What changed was the cost of the alternative — and once code-exchange interception had its own fix and moving the token server-side stopped requiring extra infrastructure most teams didn’t already run, keeping the token out of the browser entirely stopped being a hard answer and became the obvious one. If you inherited a codebase that keeps the access token in localStorage behind a PKCE flow, the fix isn’t a better-sounding storage API — it’s asking why the token is in the browser’s reach in the first place.
Table: What Each Step Actually Fixes
| Mechanism | What It Actually Targets | Where the Token Ends Up |
|---|---|---|
| Implicit Grant (RFC 6749 §4.2) | Faster issuance — skips a round trip to a token endpoint | Browser — delivered via the URL fragment |
| Authorization Code + PKCE (RFC 7636) | Binds the code exchange to the client instance that started it, blocking interception | Browser — wherever the app’s own storage code puts it |
| Backend for Frontend (RFC 10017 §6.1) | Removes the token from browser-reachable storage entirely | Server — behind an ordinary session cookie |
❓ Frequently Asked Questions
What’s the actual difference between an access token and a refresh token? An access token is what your app sends with API calls right now; it’s usually short-lived (minutes to a few hours). A refresh token is used to get new access tokens later without forcing the user to log in again, and it’s typically valid much longer — RFC 10017 gives “multiple hours or even days” as a not-uncommon lifetime for browser-based clients — which is exactly why it’s the more dangerous of the two to expose.
If I’ve added PKCE to my single-page app, is my OAuth token storage now secure? No — and this is the misconception this whole article untangles. PKCE stops an outside attacker from intercepting and completing your authorization code exchange. It says nothing about where the resulting token sits afterward, because that was never the problem PKCE was built to solve; RFC 7636’s own scope statement is about code interception, not storage.
Was it always wrong to put an access token in the browser? No, and it’s worth resisting the smug version of this story. The Implicit Grant was a reasonable answer to a real 2012-era constraint — browsers couldn’t cleanly manipulate the URL without a reload, and cross-origin requests to a token endpoint weren’t reliably available. The advice changed because those two limitations went away, not because anyone discovered the browser had suddenly become more hostile.
Is “the implicit grant is deprecated” a fair thing to say? It depends which document you’re citing, and the exact strength varies more than most summaries suggest — from a document that never uses the word “deprecated” at all, to a conditional recommendation with an escape clause, to an unconditional prohibition. Naming which specification you mean matters here.
Does moving to a BFF make my app immune to cross-site scripting? No, and no architecture in this hub claims that. A BFF removes an entire category of risk — there’s no token in browser storage for a script to steal — but it doesn’t make an application immune to script running in its own origin more generally. That’s the same residual condition every cookie-authenticated browser application has always had, BFF or not, and it deserves its own honest treatment rather than a one-line promise here.
What’s a Token-Mediating Backend, and is it the same thing as a full BFF? No. Both use a confidential backend to run the OAuth exchange, but a Token-Mediating Backend then hands the access token to the browser to call resource servers directly, while a full BFF keeps the token server-side and proxies every request itself. It’s a genuine middle position with its own trade-offs — worth evaluating deliberately, not defaulting into.
Dated 2026-09-18. RFC 6749 (October 2012, Standards Track), RFC 7636 (September 2015, Standards Track), and RFC 10017 (August 2026, BCP 212, Best Current Practice) were fetched and quoted directly from rfc-editor.org for this article.
Recommended Resources
Disclosure: some links below are Amazon affiliate links. If you buy through them, this site earns a small commission at no extra cost to you.
The best resources on this specific question are free, and better than most paid alternatives — start here before spending money:
- RFC 7636 — Proof Key for Code Exchange by OAuth Public Clients — the actual PKCE specification quoted throughout this article. Short enough to read in one sitting, and the fastest way to see for yourself exactly what it does and doesn’t claim.
- RFC 10017 — OAuth 2.0 for Browser-Based Applications — the 2026 best-practice document that formalizes the BFF pattern and walks through the storage and threat analysis this article draws from. Dense, but written for practitioners, not lawyers.
- RFC 6749 — The OAuth 2.0 Authorization Framework — the original 2012 specification, worth reading once just to see how candidly it already flagged the Implicit Grant’s trade-off years before “deprecated” entered anyone’s vocabulary.
- OWASP OAuth2 Cheat Sheet — a practical, vendor-neutral checklist for reviewing an OAuth implementation, useful as a second pass after reading the RFCs above.
If you want a book that goes deeper than any single RFC and covers OAuth implementation end to end, including the token-storage decision this article is about:
- OAuth 2 in Action by Justin Richer and Antonio Sanso (Manning, 2017). Title and authors confirmed against Manning’s own product page (
manning.com, fetched 2026-09-17), which lists ISBN-13 9781617293276 for this book — and the link above is built from that number. Publisher-confirmed, availability not checked: the/dp/page returned Amazon’s generic bot-wall (a 3,781-byte “Amazon.com” placeholder, no product content) on two separate fetches 2026-09-18, so the listing itself could not be independently read — only that Amazon answered with HTTP 200. This is a first-edition-only title with no later edition to check against; it predates RFC 10017 and PKCE’s browser-app generalization, so treat its storage-location advice as historical context alongside the RFCs above, not a substitute for them.