• Terms of Service

Learn Internet Grow

Tech Skills

  • Learn
  • Books
  • Search
  • Create
  • Measure
  • Secure
  • Share
  • Terms of Service

What Is OAuth Token Storage? An On-Ramp for Developers

Filed Under: Learn

A brass key resting exactly on the boundary between a lit tile and a shadowed tile, representing the decision of where a credential should live.

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.

Two identical closed lockboxes on a wooden desk, one lit warmly in front and one in cool shadow behind, representing a choice between equivalent storage locations.

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.

A glowing paper lantern drifting openly down a dusk street past silhouetted onlookers, with a single lit doorway visible in the distance.

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.

A single freshly locked door in a long stone corridor of otherwise identical, unupgraded doors.

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.

A hidden wall panel standing silently open on its own, revealing a freshly made duplicate key resting on a shelf in a small inner room.

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.

A service counter with an empty hand resting on the public side, while a vault of keys stays visible but out of reach on the far side.

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

MechanismWhat It Actually TargetsWhere the Token Ends Up
Implicit Grant (RFC 6749 §4.2)Faster issuance — skips a round trip to a token endpointBrowser — delivered via the URL fragment
Authorization Code + PKCE (RFC 7636)Binds the code exchange to the client instance that started it, blocking interceptionBrowser — wherever the app’s own storage code puts it
Backend for Frontend (RFC 10017 §6.1)Removes the token from browser-reachable storage entirelyServer — 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.

Newsletter

Tags

Agentic AI Agentic Development AI AI Agents AI Coding Agents Blockchain Bounded Context ChatGPT Plugins Claude Code CLPR Context Engineering Cryptocurrency DePin Developer Tools Distributed Systems DLT Domain-Driven Design Ethernet Google Search HBAR Hedera Home Networking Internet Internet Performance LLM Local Area Networks Machine Learning MCP Model Context Protocol Network Architecture Network Diagnostics Network Security News NLP Postgres PostgreSQL RAG Routers Saga Pattern SEO Software Architecture software engineering Troubleshooting Web3 Wi-Fi

Recent Articles

  • Is the OAuth Implicit Flow Deprecated? One Word, Four Documents, and Only One MUST NOT November 7, 2026
  • Your Notebook Ran. That Isn’t a Result. November 7, 2026
  • DOCSIS Uploads and Multi-Gig: Find the Real Bottleneck November 7, 2026
  • AI Agent Memory: Search and RAG Limits November 6, 2026
  • Plan Locally, Execute in the Cloud: Claude Code’s Own Recommended Workflow November 6, 2026
  • The AI Evaluation Scorer That Never Read a Transcript November 6, 2026

Copyright © 2026 · News Pro Theme On Genesis Framework · WordPress · Log in

Content on this site is produced with the assistance of Large Language Models and edited by a human. It is intended for educational purposes — facts may occasionally be incorrect. If you spot an error, please email Larionovdmitri@learninternetgrow.com.

Affiliate Disclosure: This site contains affiliate links to Amazon and other partners. If you purchase through these links, LearnInternetGrow.com may earn a commission at no additional cost to you. We only recommend products we genuinely believe in.

Privacy Policy · Terms of Service