- RFC 10017: OAuth 2.0 for Browser-Based ApplicationsParecki, De Ryck & Waite (IETF, August 2026) - part of BCP 212, with RFC 8252; sections 5, 6 and 8 carry everything above
- RFC 8252: OAuth 2.0 for Native Appsthe other half of BCP 212 - the native sibling, covered in depth on its own page
- RFC 9700: Best Current Practice for OAuth 2.0 Securitythe floor this document "expands on and further restricts" for browser clients (section 1)
- RFC 7636: Proof Key for Code Exchangethe MUST behind PKCE everywhere - demonstrated live in the PKCE console
- RFC 9449: DPoPthe sender-constraining layer this threat model keeps invoking - the deep dive
Published August 2026 (Parecki, De Ryck & Waite), RFC 10017 is the newest document in the suite - and half of a bundle: it shares BCP 212 withRFC 8252, the native-apps BCP. One BCP, two client platforms. Where 8252 answered where does the redirect land when the app has no server?, 10017 answers the SPA's version: where do the tokens live when the client is the browser?
Like 8252 it is a best current practice, not a new grant: the client is still doing the authorization-code grant you know, usually with PKCE. What the document adds is the part SPA tutorials skip - a threat model for the one attacker browsers can never fully keep out (malicious JavaScript), a ranking of the architectures by what survives that attacker, and honest guidance on token storage. Its recommendations are "derived from the Best Current Practice for OAuth 2.0 Security [RFC9700]", which browser apps are expected to follow already - the document "expands on and further restricts various recommendations" (section 1).
Section 5 starts from the fact that makes browser clients different from every other OAuth client: XSS or a compromised third-party script gives an attacker code in your origin, and "the malicious JavaScript code has the same privileges as the legitimate application code" (section 5). Application-level defenses - CSP, sanitization, SRI - are how you avoid that foothold, and section 5 opens with them. But history says some attacker eventually gets in, so the document analyzes what code-in-your-origin can actually do to an OAuth client. Four attack scenarios (section 5.1):
| Scenario | The move | What blunts it |
|---|---|---|
| Single-execution token theft section 5.1.1 | Read the current tokens from storage, ship them to the attacker's server, done | Short lifetimes, refresh token rotation (RFC 9700 §4.14.2), sender-constrained tokens - the classic defenses |
| Persistent token theft section 5.1.2 | Same, but on a timer: the attacker keeps harvesting the latest tokens, and can stop the app from ever using the newest refresh token so rotation never detects the theft | Short lifetimes and rotation are exactly the "typical defenses against token theft" this slight variation "already suffices to counter" (5.1.2) |
| Acquisition and extraction of new tokens section 5.1.3 | Skip your tokens entirely: a hidden iframe runs a silent authorization-code flow inside the user's session and mints fresh ones | "There are no practical security mechanisms for frontend applications that counter this attack scenario" - DPoP included, since the attacker signs with their own key |
| Proxying requests via the user's browser section 5.1.4 | No theft at all: call the API from inside the app and read the responses; the requests are indistinguishable from the legitimate client's | Nothing client-side - the same unsolved problem as HttpOnly session cookies; DPoP "explicitly considers this attack scenario to be out of scope" |
The scenario that changes the argument is 5.1.3: "even if the application finds a token storage mechanism that is able to completely isolate the stored tokens from the attacker, the attacker will still be able to request a new set of tokens" (section 5.1.3). Perfect storage does not save a pure browser client. That is what makes the next section an architecture ranking and not a storage tips list.
Section 6 lays out three patterns and is explicit about the ordering: each "offers a different trade-off between security and simplicity" and they are "presented in decreasing order of security" (section 6). The honest part is that the most secure one is also the most work:
| Rank | Pattern | Where the tokens live | The trade-off, honestly |
|---|---|---|---|
| 1 | Backend for Frontend (BFF) section 6.1 | Server-side only. The BFF is a confidential client; the browser holds an HttpOnly session cookie and never a token | Survives 5.1.1-5.1.3 outright; 5.1.4 remains but cannot escalate to session theft. Cost: "significantly more complicated" - a server component to run and every API call proxied (6.1.4.3). Verdict: "strongly recommended for business applications, sensitive applications, and applications that handle personal data" |
| 2 | Token-mediating backend section 6.2 | Backend obtains and stores the refresh token as a confidential client, then hands the app short-lived access tokens | "Less secure than a BFF but still offers significant advantages over an OAuth client application running directly in the browser" (6.2). No proxying duty - but the access token is exposed to the browser, so 5.1.1, 5.1.2 and 5.1.4 apply to it |
| 3 | Browser-based OAuth 2.0 client (the "pure" SPA) section 6.3 | Everything in the browser: public client, tokens in browser storage, refresh token included | "Inherently vulnerable to malicious browser-based code" (6.3.4.2). Viable when there is no backend at all - with storage picked to slow the attacker down, not to stop them |
Read as a decision, not a leaderboard: if you have (or can add) a backend, the BFF takes tokens out of the attacker's reach entirely, and the token-mediating backend is the lighter middle ground that at least keeps refresh tokens server-side. The pure client is not forbidden - the document devotes detailed requirements to it (section 6.3.2) - but every defense it lists comes with the same footnote: none of them "address or fix the underlying problem" (6.3.4.2).
When tokens do live in the browser, section 8 ranks the places to put them. The ladder, from more to less isolated: a service worker or web worker keeping tokens out of the page's execution context (8.2, 8.3); in-memory with a closure shield (8.4, with the honest warning that prototype pollution can still reach in); sessionStorage, which at least dies with the tab and stays out of other tabs; localStorage and IndexedDB, readable by any same-origin code (8.5). Cookies are their own dual nature - storageand transport - and the JavaScript Cookie API is "often mistaken as an easy way to store data in the browser" (8.1): a token in a JS-set cookie is just localStorage with extra steps.
And then the verdict that the whole section earns: "none of these options can fully mitigate token exfiltration when the attacker can execute malicious code in the application's execution environment" (8.5). Storage choice changes how hard the attacker works; architecture changes whether they win. That is the argument for the BFF in one sentence - and the reason a bearer token'sthreat list reads so differently once the client is a browser.
For the pure browser client, section 6.3.2 turns habits into requirements, and the first one is the suite's most repeated MUST:
"Browser-based applications that are public clients MUST implement the PKCE [RFC7636] extension when obtaining an access token, and authorization servers MUST support and enforce PKCE for such clients" (6.3.2.1). PKCE does not solve section 5 - it solves the older, adjacent attack where something else intercepts your authorization code. The habit generalizes: native apps MUST use it (RFC 8252 §8.1), every public client MUST under RFC 9700 §2.1.1, OAuth 2.1 extends it to all clients, and this document keeps the streak alive for SPAs. At this point "PKCE everywhere" is not advice; it is what conformance means.
Section 6.3.2.2 adds the second habit: CSRF protection on the redirect URI - PKCE or a verified state value (or OIDC nonce), not vibes. You can watch PKCE absorb a code-theft attempt in thePKCE console - the interception attack it was built for, with tamper buttons.