Why you SPA shouldn't handle OAuth tokens
Why Your SPA Shouldn’t Handle OAuth Tokens
Most OAuth tutorials for SPAs show you how to get an access token and store it in localStorage. The app works. You ship it.
The Problem with Browser-Based OAuth Clients
When your SPA handles OAuth directly, it acts as a “public client” and has no secure way to store credentials. The tokens end up in one of the browser's storage areas: localStorage or sessionStorage. Wherever they land, any JavaScript running on your page can access them.
This includes:
Malicious scripts from XSS vulnerabilities
Compromised third-party libraries
Injected code from browser extensions
The IETF draft on browser-based applications is explicit: browser-based public clients are “not recommended for business applications, sensitive applications, and applications that handle personal data.” A large percentage of applications fall into this space
The Attack That Changes Everything
You might think: “I’ll just use short-lived access tokens and refresh token rotation. Even if tokens get stolen, the damage is limited.”
That’s true for simple theft. But there’s a more sophisticated attack that bypasses all of these defences.
An attacker with XSS on your page doesn’t need to steal your tokens. They can get their own.
Here’s how:
Inject a hidden iframe
Initiate a silent OAuth flow using the user’s existing session
Extract the authorisation code from the iframe
Exchange it for a fresh set of tokens
The attacker now has their own access token and refresh token, utterly independent of yours. Short token lifetimes don’t help. Refresh token rotation doesn’t help. PKCE doesn’t help. DPoP doesn’t help.
Why? Because the attacker is running a legitimate OAuth flow. They’re just doing it from your origin, with your user’s session.
The Backend for Frontend Pattern
The BFF pattern takes a fundamentally different approach. Instead of your SPA acting as the OAuth client, a backend component handles all OAuth responsibilities.
The BFF has three jobs:
Act as a confidential OAuth client (with real credentials)
Store tokens server-side, tied to a session
Proxy all API calls, attaching the access token before forwarding
Your SPA never sees a token. It only receives an HttpOnly session cookie.
Why This Stops the Attack
The silent flow attack fails because the BFF is a confidential client. Even if the attacker obtains an authorisation code, they can’t exchange it because they don’t have the client secret.
With a public client, all four attack scenarios apply: stealing existing tokens, stealing tokens continuously, running a silent flow for new tokens, and proxying requests through the user’s browser.
With a BFF, only the last one remains. And that’s not an OAuth vulnerability; it’s inherent to all web applications. The attacker can make requests while the user’s browser is open, but they can’t exfiltrate credentials for later use.
When to Use the BFF Pattern
If you’re building business applications, sensitive applications, or anything that handles personal data, use a backend to handle OAuth.
In practice, this means:
Financial services
Healthcare applications
Enterprise software
Any app with user data you’d rather not see in a breach notification
The BFF adds infrastructure complexity. You need a backend component, session storage, and a proxy layer. But this complexity provides security guarantees that browser-based OAuth simply cannot.
The Middle Ground
The decision of whether to use a BFF is not binary. A pure SPA with browser-based OAuth is at one end of the spectrum, while a BFF is at the other. In between the two, there are other options you can employ. Here is one of them:
Token-Mediating Backend
This architecture acts as a “middle ground” between a full BFF and a browser-only client. It is lighter-weight than a BFF because it does not require proxying every API request through your server, but it is less secure because the access token is exposed to the browser.
• How it works: You still use a backend component to handle the OAuth exchange (exchanging the authorisation code for tokens), acting as a confidential client. However, instead of keeping the access token hidden, the backend passes it to the browser application. The browser then uses this token to call resource servers directly.
• Security Properties:
◦ Refresh Tokens: The backend keeps the refresh token and does not expose it to the browser, protecting it from theft via XSS. When the access token expires, the browser requests a new one from the backend.
◦ Access Tokens: The access token is exposed to the browser, making it vulnerable to theft if malicious scripts compromise the application.
◦ Hijacking: Because the access token is exposed, an attacker could steal it to call APIs directly, unlike a pure BFF, where the attacker can only hijack the client session.
• Recommendation: This pattern is recommended only if the use cases or system requirements prevent the use of a proxying BFF.
Analogy
Please think of the BFF as a bank teller who keeps the vault key (token) behind the counter; you ask them to perform transactions, and they do it for you. The Token-Mediating Backend is like a manager who gets the key from the vault but hands it to you to open the safety deposit box yourself; you have more direct access, but if someone steals the key from you, they can open the box too. The Browser-Based Client is like having the key mailed directly to your house; it’s convenient, but anyone who breaks into your mailbox (browser) gets the key immediately.


