Most SPA security discussions still start with the wrong question.
The question is not whether a browser application can use OAuth. Modern browser applications can use the Authorization Code flow with PKCE. RFC 7636 defines PKCE for public clients, and the OAuth 2.0 Security Best Current Practice makes the code flow, exact redirect handling, and stronger token handling part of the modern baseline.
The better question is where the tokens live.
If an access token or refresh token is available to browser JavaScript, then any script that executes in that origin becomes part of the token boundary. That includes application code, dependencies, tag managers, injected scripts, compromised packages, browser extensions, and XSS payloads. PKCE protects the authorization code exchange. It does not make a token safe after the browser owns it.
PKCE does not move the boundary
PKCE is important. It prevents a stolen authorization code from being redeemed by a different client instance. For public clients, including browser-based applications that cannot hold a client secret, that is a necessary improvement over older OAuth examples.
But PKCE is not a storage strategy.
Once the browser application has received an access token, the token must live somewhere the application can reach it: memory, a worker, browser storage, or an abstraction over those choices. Each option has different persistence and exposure properties, but they all share the same hard constraint: if malicious code can run with enough access inside the application origin, it may be able to read the token, trigger a new flow, or call APIs as the user.
That is the browser token problem in one sentence: the security boundary is not the login redirect. The security boundary is the place where tokens become reachable.
The browser threat model is not theoretical
The IETF OAuth working group draft OAuth 2.0 for Browser-Based Applications treats malicious JavaScript as a central threat for browser OAuth clients. It is an Internet-Draft rather than a final RFC, so it should be cited as work in progress. Its architecture analysis is still useful because it names the problem directly: a browser-based application is not a confidential environment.
An attacker who can execute JavaScript in the application origin may do more than copy the token currently sitting in storage. The draft discusses several OAuth-specific outcomes:
- steal the current access or refresh token
- keep stealing newly rotated tokens while the user is active
- start a new authorization flow from the compromised origin
- proxy actions through the user’s browser
Those are different failures. A short access-token lifetime helps with a copied access token. Refresh-token rotation helps detect some replay. Sender-constrained access tokens can reduce the replay value of a stolen token. None of those controls fully answers the case where the browser itself is actively executing attacker-controlled code.
That is why browser token handling is an architecture decision, not just an SDK configuration choice.
Three patterns, three boundaries
The browser-based apps draft describes three main architecture patterns for browser applications that use OAuth to access protected resources:
| Pattern | Where OAuth tokens live | What the browser sends to APIs | Boundary strength |
|---|---|---|---|
| Full Backend for Frontend | Server-side, associated with an application session | Requests to the BFF; the BFF calls APIs | Strongest boundary, higher operational cost |
| Token-mediating backend | Backend handles OAuth and refresh tokens, browser still receives access tokens | Direct API calls with access tokens | Partial mitigation |
| Browser-only OAuth client | Browser handles the OAuth flow and stores tokens | Direct API calls with access tokens | Simplest, highest token exposure |
The pattern names matter less than the boundary they create.
In a browser-only client, the SPA is the OAuth client. It receives tokens and calls APIs directly. This can be appropriate for lower-risk applications when the team accepts the residual exposure and keeps tokens short-lived, narrowly scoped, and carefully handled. It is not the same thing as saying the browser is a confidential client.
In a token-mediating backend, the backend handles the authorization code exchange and can protect refresh tokens server-side. That is an improvement. The browser can avoid long-lived refresh-token storage, and the backend can act as a confidential client. But if the browser still receives access tokens and calls APIs directly, malicious JavaScript can still steal or use those access tokens.
In a full BFF pattern, the browser is no longer the OAuth token holder. The backend acts as the OAuth client, stores tokens server-side, maintains the application session, and forwards API requests with the right access token. The browser receives an application session cookie rather than OAuth tokens.
flowchart LR
subgraph Direct["Browser-only or token-exposing SPA"]
Browser1["Browser SPA<br/>JavaScript runtime"]
Token1["Access token<br/>reachable by browser code"]
Api1["Resource API"]
Browser1 --> Token1
Browser1 -->|"Authorization: Bearer token"| Api1
Mal1["Malicious JavaScript"] -. "can read or use token" .-> Token1
Mal1 -. "can call API as user" .-> Api1
end
subgraph Bff["Full BFF boundary"]
Browser2["Browser SPA<br/>session cookie"]
Backend["Backend for Frontend<br/>confidential OAuth client"]
Token2["Access and refresh tokens<br/>server-side"]
Api2["Resource API"]
Browser2 -->|"same-origin app request"| Backend
Backend --> Token2
Backend -->|"Authorization: Bearer token"| Api2
Mal2["Malicious JavaScript"] -. "no direct token access" .-> Backend
end
classDef exposed fill:#fff4e5,stroke:#b45309,color:#3f2a00
classDef contained fill:#eef8f2,stroke:#15803d,color:#12351f
class Browser1,Token1,Api1,Mal1 exposed
class Browser2,Backend,Token2,Api2,Mal2 contained
The diagram is deliberately plain. A BFF does not make XSS harmless. If attacker code runs in the user’s browser, it may still trigger requests through the BFF while the user’s session is active. It may still inspect page data. It may still abuse application behavior.
The difference is narrower and important: the attacker does not get the OAuth access token simply because the SPA needed to call an API.
Why the full BFF is usually the stronger default
The full BFF pattern moves OAuth responsibilities into a server-side component that can be defended with ordinary backend controls: confidential client authentication, server-side token storage, central refresh handling, audit logging, deployment policy, and network restrictions.
For the browser, the security model becomes closer to a traditional web application session. The browser sends a cookie to the BFF. The BFF validates the session, chooses the correct API token server-side, calls the API, and returns data to the frontend. OAuth tokens do not appear in browser storage or application state.
This changes several review questions:
- Can access tokens be copied from browser storage or DevTools?
- Can a refresh token survive after the user’s tab closes?
- Can a third-party script send the token to a different origin?
- Can API calls be centralized behind one same-origin backend?
- Can CSRF defenses, cookie attributes, and CORS policy be reviewed in one place?
The answer is not automatically “BFF everywhere.” A BFF introduces operational work: session storage, proxy routes, API aggregation, latency considerations, CSRF protection, cookie configuration, logout behavior, and ownership of the frontend backend. If a team treats the BFF as a generic pass-through proxy without clear ownership, it can become another fragile layer.
Still, for applications that handle sensitive data and can route API calls through a backend, the token boundary is usually worth the operational cost.
Token-mediating backends are not the same thing
Token-mediating backends are attractive because they improve one part of the model without forcing every API call through the backend. The backend can perform the OAuth flow as a confidential client. It can keep refresh tokens out of the browser. It can issue or return short-lived access tokens to the SPA when needed.
That is a real mitigation, especially for applications that cannot proxy all API traffic for latency, topology, ownership, or deployment reasons.
But it leaves the access-token boundary in the browser. If the SPA receives an access token, JavaScript can use it. If malicious JavaScript can use it, the API has to assume that a request carrying the token might not represent the intended application behavior.
The browser-based apps draft is explicit on this tradeoff: teams should evaluate whether a full BFF is viable before choosing a token-mediating backend. Only use the partial pattern when system requirements really prevent a proxying BFF.
How to decide for an existing SPA
A practical architecture review does not need to start with a rewrite. Start by naming the boundary you have today.
Ask five questions:
- Does the browser receive access tokens?
- Does the browser receive refresh tokens?
- Are tokens persisted beyond the active page lifetime?
- Can the APIs be called through a same-origin backend?
- What data or actions become available if attacker code runs in the SPA origin?
Then classify the application.
For public, low-risk data and simple API calls, a browser-only OAuth client with authorization code plus PKCE, short token lifetimes, narrow scopes, strict redirect handling, and careful storage may be a reasonable tradeoff.
For applications with personal data, customer administration, billing, security settings, or operational controls, direct browser token possession should be challenged. A full BFF is often the cleaner target because it removes access and refresh tokens from browser JavaScript.
For applications that cannot immediately move to a BFF, a token-mediating backend can be a transition step. Move refresh tokens server-side first. Stop persisting long-lived tokens in the browser. Reduce access-token lifetime and scope. Tighten API CORS policy. Treat every remaining browser-held access token as an intentional residual risk, not a harmless implementation detail.
A migration path that does not pretend risk disappears
The useful migration path is usually incremental:
- Inventory every browser application that receives OAuth tokens.
- Separate ID tokens, access tokens, and refresh tokens in the review. They serve different purposes and create different exposure.
- Remove implicit flow usage from new designs and prefer authorization code with PKCE.
- Stop issuing refresh tokens to browser-only clients unless there is a reviewed reason and the authorization server applies the protections described in RFC 9700.
- For sensitive applications, introduce a BFF boundary and move token storage server-side.
- Keep browser defenses in place: Content Security Policy, dependency review, script governance, output encoding, and incident response for XSS.
That last point matters. A BFF changes token exposure. It does not remove the need to build the frontend safely. The browser remains a powerful execution environment, and a compromised origin can still perform user-visible actions while the session is active.
The value of the BFF boundary is more precise: tokens stop being browser assets. They become server-side credentials used by a component the platform team can operate, monitor, and harden.
The architecture question to ask
OAuth security has moved beyond “did we add PKCE?” PKCE is table stakes. The harder question is whether the application’s token boundary matches the value of the data and actions behind the API.
For browser applications, that question is concrete:
Can malicious JavaScript reach the token, or only the application session?
If it can reach the token, the system is relying on every script in the browser origin to behave as part of the OAuth client. Sometimes that is an accepted tradeoff. For serious business applications, it is often the wrong boundary.
For implementation context, start with the ProAuth documentation for client applications and the standards references above: RFC 9700, RFC 7636, RFC 6750, and the IETF work-in-progress draft OAuth 2.0 for Browser-Based Applications.