BFF For SPA
BFF For SPA
The architectural pattern in which the Backend for Frontend acts as the OAuth2 confidential client on behalf of a Single Page Application, so that the SPA never handles, stores, or sees OAuth2 tokens — also called the Token Handler Pattern.
Core Idea
Single Page Applications run entirely in the browser — a fundamentally untrusted execution environment. Any JavaScript code running in the same origin, including third-party scripts, can read localStorage, sessionStorage, and in-memory JavaScript variables. This makes it impossible for a SPA to safely hold OAuth2 tokens.
The BFF-for-SPA pattern solves this by moving the OAuth2 client role entirely to a server-side BFF. The SPA delegates authentication to the BFF and receives back only an opaque, httpOnly session cookie. Tokens never reach browser JavaScript at any point in the flow.
Why It Exists
The Problem with Tokens in the Browser
| Storage Location | Attack Vector | Consequence |
|---|---|---|
localStorage | XSS — any injected script can call localStorage.getItem() | Attacker steals access and refresh tokens; user account fully compromised |
sessionStorage | XSS — same as localStorage, scoped to tab only | Same as above, with the addition of tokens lost on tab close |
| JavaScript in-memory variable | XSS — same execution context | Tokens stolen; additionally lost on every page refresh, requiring complex refresh orchestration in Angular |
httpOnly session cookie (BFF) | Cookie is unreadable by JavaScript | XSS cannot access tokens; CSRF mitigated by SameSite + CSRF token |
The httpOnly session cookie approach is only viable when there is a server-side BFF to manage the token on the browser's behalf.
Why the BFF Can Do This Safely
The BFF is a server-side process. It:
- Runs outside the browser's JavaScript execution environment
- Can hold a
client_secretin environment variables - Can use Redis for durable, shareable session storage
- Can set
httpOnlycookies that JavaScript cannot read - Can transparently refresh tokens without the SPA knowing
The Full Flow (10 Steps)
Angular SPA BFF (Spring Cloud Gateway) IdP (Keycloak)
| | |
1. |-- GET /oauth2/authorization/keycloak -------------->|
| | |
2. | BFF generates code_verifier, stores in session |
| BFF computes code_challenge (SHA-256) |
| BFF redirects browser to IdP with PKCE params |
|<-- 302 to IdP /authorize?code_challenge=... --------|
| |
3. |-- (browser follows redirect to IdP) --------------->|
| |
| User authenticates at IdP |
| |
4. |<-- 302 /login/oauth2/code/keycloak?code=XYZ --------|
|-- GET /login/oauth2/code/keycloak?code=XYZ -------->|
| | |
5. | BFF retrieves code_verifier from session |
| BFF exchanges: code + code_verifier |
| |--- POST /token ---------->|
| |<-- access_token + refresh_token + id_token
| | |
6. | BFF stores tokens in Redis session |
| (session key → {access_token, refresh_token, expires_at})
| | |
7. |<-- 302 + Set-Cookie: SESSION=<opaque-id>; httpOnly; Secure; SameSite=Lax
| | |
8. |-- GET /api/dashboard (cookie sent automatically) -->|
| | |
9. | BFF reads session → retrieves access_token
| BFF adds: Authorization: Bearer <access_token>
| BFF forwards request to downstream service
| | |
|<-- 200 {dashboard data} ----------------------- ----|
| | |
10. |-- GET /api/orders (token silently expired) -------->|
| BFF detects expiry, calls IdP with refresh_token
| BFF stores new access_token in Redis
| BFF forwards request with fresh token
|<-- 200 {order data} -------------------------------|
Step-by-Step Explanation
-
SPA navigates to
/oauth2/authorization/{provider}on BFF — this is a standard Spring Security endpoint; the SPA redirects the browser (not an AJAX call) to this URL. -
BFF redirects to IdP with PKCE challenge (S256 method) — Spring Security generates a cryptographically random
code_verifier, stores it in the server-side session, computescode_challenge = BASE64URL(SHA256(code_verifier)), and redirects the browser to the IdP's/authorizeendpoint with the challenge. See PKCE. -
User authenticates at IdP — the browser is now on the IdP's login page. The BFF and the SPA are not involved. The IdP stores the
code_challengeassociated with the issued authorization code. -
IdP redirects back to BFF
/login/oauth2/code/{provider}with authorization code — the browser follows the redirect back to the BFF. The BFF receives theauthorization_codeas a query parameter. -
BFF exchanges code + PKCE verifier for tokens (server-side only) — the BFF retrieves the
code_verifierfrom the session, then makes a server-to-server POST to the IdP's token endpoint, sendingcode,code_verifier, andclient_secret. The IdP verifiesSHA256(code_verifier) == stored code_challengeand returnsaccess_token,refresh_token, andid_token. This exchange is entirely server-side; the tokens never pass through the browser. -
BFF stores tokens in Redis-backed server-side session — Spring Security stores the
OAuth2AuthorizedClient(which holds the tokens) inServerOAuth2AuthorizedClientRepository, which persists to Redis via Spring Session. Multiple BFF instances share the same Redis store, enabling horizontal scaling. -
BFF sets
httpOnly + Secure + SameSite=Laxsession cookie on response — the browser receives a redirect to the original destination and aSet-Cookieheader with an opaque session ID. ThehttpOnlyattribute prevents JavaScript from reading the cookie value. TheSecureattribute ensures the cookie is only transmitted over HTTPS.SameSite=Laxprevents the cookie from being sent in cross-site POST requests. -
SPA makes subsequent API calls through BFF using session cookie (automatic) — the browser automatically includes the session cookie on all requests to the BFF origin. The SPA issues ordinary fetch/HttpClient calls with no special token handling.
-
BFF retrieves token from session, adds
Authorization: Bearerheader, forwards to downstream — the Token-Relay-Pattern is in effect. TheTokenRelay=Gateway filter or theServerOAuth2AuthorizedClientExchangeFilterFunctionhandles this for every route. -
BFF silently refreshes expired tokens using
refresh_token— SPA is unaware — when theaccess_tokenhas expired, theReactiveOAuth2AuthorizedClientManagerdetects this before or after a 401 from downstream, calls the IdP's token endpoint with therefresh_token, stores the new token pair in Redis, and retries the request. The SPA sees a successful API response.
Security Properties Achieved
| Property | Mechanism |
|---|---|
| Tokens never reach browser JavaScript | httpOnly session cookie carries only an opaque session ID; tokens live in Redis |
| XSS cannot steal tokens | JavaScript cannot read httpOnly cookies; no tokens in localStorage or memory |
| CSRF mitigated | SameSite=Lax cookie blocks cross-site POST requests; Spring Security CSRF filter enforces X-XSRF-TOKEN header for state-changing requests |
| Token refresh is invisible to the SPA | BFF handles refresh transparently; Angular never sees a 401 caused by token expiry |
| Single logout revokes all downstream access | Invalidating the BFF session in Redis immediately removes the stored tokens; no downstream service can be called without a valid session |
| Code interception prevented | PKCE binds the authorization code to the BFF session; stolen codes are unusable without the code_verifier |
client_secret never exposed | Secret lives in BFF environment variables; never transmitted to browser |
Tradeoffs
What the BFF-for-SPA Pattern Costs
BFF must be stateful (Redis session required) : Pure stateless BFF deployments are not possible with this pattern. Redis must be provisioned, operated, and made highly available. Session replication is required for horizontal scaling. See Spring-Cloud-Gateway deployment notes.
Additional network hop for every SPA request : All SPA API traffic routes through the BFF before reaching downstream services. This adds latency (typically 1–5ms for a co-located BFF) and means the BFF is in the critical path for all user-facing operations.
BFF becomes a shared infrastructure component requiring high availability : The BFF is now both the API proxy and the authentication entry point. A BFF outage means users cannot authenticate and cannot access any API. This demands proper redundancy (multiple BFF instances behind a load balancer), health checks, and operational monitoring.
Session management complexity : Absolute session timeout, sliding window timeout, and session fixation prevention all require explicit configuration. Redis memory management and session eviction policies must be understood.
BFF-for-SPA vs. Tokens in the Browser
| Approach | Token Storage | XSS Risk | Refresh Strategy | Complexity |
|---|---|---|---|---|
| BFF-for-SPA (this pattern) | Redis (server-side) | None — tokens never in browser | BFF handles transparently | Higher infrastructure; simpler Angular code |
| SPA + PKCE (no BFF), localStorage | localStorage | HIGH — XSS steals persistent tokens | Angular must implement refresh loop | Lower infrastructure; higher Angular complexity + security risk |
| SPA + PKCE (no BFF), in-memory | JavaScript variable | LOW for theft, but lost on refresh | Complex — Angular must implement refresh before expiry AND on page load | No infrastructure; highest Angular complexity |
| Implicit flow (deprecated) | URL fragment → memory | HIGH | Not available (no refresh token) | Avoid entirely — deprecated in OAuth 2.1 |
The BFF-for-SPA pattern is the recommendation of RFC 9700 (OAuth Security BCP), Philippe De Ryck (pragmaticwebsecurity.com), and the OWASP ASVS for modern SPA deployments.
Spring Boot Implementation Summary
The BFF-for-SPA pattern is implemented in Spring Boot + Spring Cloud Gateway using three key dependencies:
<!-- OAuth2 client: handles Authorization Code Flow, PKCE, token storage -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!-- Spring Session + Redis: distributed, httpOnly-cookie-backed session -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- Token Relay filter: injects Bearer token into proxied requests -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>For full configuration, see P4-BFF-Security.
Common Misconceptions
"SPAs can use PKCE without a BFF and be equally secure"— PKCE alone does not solve token storage. A SPA using PKCE without a BFF must still store the resulting tokens somewhere in the browser. The BFF eliminates token storage in the browser entirely."This pattern requires the SPA to send tokens"— The SPA never sees tokens. It only sends the session cookie, which is attached automatically by the browser."The BFF-for-SPA pattern is over-engineered for small applications"— The security properties (no tokens in browser, transparent refresh, single logout) are worth the infrastructure investment in virtually all production contexts. The pattern can be implemented with a single Spring Boot + Redis deployment."Disabling CSRF is fine because the SPA sends JSON"— Content-type checks are not a reliable CSRF defense. TheSameSitecookie and Spring Security CSRF filter must both be properly configured.
Related Concepts
| Concept | Relationship |
|---|---|
| PKCE | Step 2 of the flow — BFF uses PKCE to bind the authorization code to its session |
| OAuth2-BFF-Pattern | The overarching pattern of BFF as OAuth2 confidential client |
| Token-Relay-Pattern | Step 9 of the flow — BFF forwards tokens to downstream services |
| Spring-Cloud-Gateway | The SCG infrastructure on which the BFF runs; provides TokenRelay= filter |
| BFF-Pattern | The general BFF architectural pattern that makes this security model possible |
Related Security Patterns
- OAuth2-OIDC-Flows — BFF-for-SPA implements Authorization Code + PKCE from this flow taxonomy; the full grant type decision tree is in OAuth2-OIDC-Flows
- Session-Management — the BFF session lifecycle (httpOnly cookie, Redis store, session rotation on privilege change) is documented in Session-Management with cookie attribute details
- CORS-CSP — SPA-to-BFF cross-origin requests require CORS configuration; CSP nonce-based policy restricts which scripts the SPA browser may load
Sources
- RFC 9700 — OAuth 2.0 Security Best Current Practice (tools.ietf.org/html/rfc9700)
- RFC 7636 — Proof Key for Code Exchange (tools.ietf.org/html/rfc7636)
- Philippe De Ryck, "The BFF Security Pattern" — pragmaticwebsecurity.com
- Okta Developer Blog, "Is the OAuth 2.0 Implicit Flow Dead?" (developer.okta.com)
- Spring Security Reference: Reactive OAuth2 Client (docs.spring.io/spring-security/reference/reactive/oauth2)
- Spring Session Reference: Spring Session with Redis (docs.spring.io/spring-session/reference)
Backlinks
- OAuth2-BFF-Pattern — OAuth2 BFF Pattern is the mechanism underlying BFF-for-SPA
- PKCE — PKCE runs at step 2 of the BFF-for-SPA flow
- Token-Relay-Pattern — Token Relay is step 9 of the BFF-for-SPA flow
- P4-BFF-Security — Phase 4 research note covering this pattern in full implementation detail