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 LocationAttack VectorConsequence
localStorageXSS — any injected script can call localStorage.getItem()Attacker steals access and refresh tokens; user account fully compromised
sessionStorageXSS — same as localStorage, scoped to tab onlySame as above, with the addition of tokens lost on tab close
JavaScript in-memory variableXSS — same execution contextTokens stolen; additionally lost on every page refresh, requiring complex refresh orchestration in Angular
httpOnly session cookie (BFF)Cookie is unreadable by JavaScriptXSS 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_secret in environment variables
  • Can use Redis for durable, shareable session storage
  • Can set httpOnly cookies 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

  1. 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.

  2. BFF redirects to IdP with PKCE challenge (S256 method) — Spring Security generates a cryptographically random code_verifier, stores it in the server-side session, computes code_challenge = BASE64URL(SHA256(code_verifier)), and redirects the browser to the IdP's /authorize endpoint with the challenge. See PKCE.

  3. 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_challenge associated with the issued authorization code.

  4. IdP redirects back to BFF /login/oauth2/code/{provider} with authorization code — the browser follows the redirect back to the BFF. The BFF receives the authorization_code as a query parameter.

  5. BFF exchanges code + PKCE verifier for tokens (server-side only) — the BFF retrieves the code_verifier from the session, then makes a server-to-server POST to the IdP's token endpoint, sending code, code_verifier, and client_secret. The IdP verifies SHA256(code_verifier) == stored code_challenge and returns access_token, refresh_token, and id_token. This exchange is entirely server-side; the tokens never pass through the browser.

  6. BFF stores tokens in Redis-backed server-side session — Spring Security stores the OAuth2AuthorizedClient (which holds the tokens) in ServerOAuth2AuthorizedClientRepository, which persists to Redis via Spring Session. Multiple BFF instances share the same Redis store, enabling horizontal scaling.

  7. BFF sets httpOnly + Secure + SameSite=Lax session cookie on response — the browser receives a redirect to the original destination and a Set-Cookie header with an opaque session ID. The httpOnly attribute prevents JavaScript from reading the cookie value. The Secure attribute ensures the cookie is only transmitted over HTTPS. SameSite=Lax prevents the cookie from being sent in cross-site POST requests.

  8. 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.

  9. BFF retrieves token from session, adds Authorization: Bearer header, forwards to downstream — the Token-Relay-Pattern is in effect. The TokenRelay= Gateway filter or the ServerOAuth2AuthorizedClientExchangeFilterFunction handles this for every route.

  10. BFF silently refreshes expired tokens using refresh_token — SPA is unaware — when the access_token has expired, the ReactiveOAuth2AuthorizedClientManager detects this before or after a 401 from downstream, calls the IdP's token endpoint with the refresh_token, stores the new token pair in Redis, and retries the request. The SPA sees a successful API response.


Security Properties Achieved

PropertyMechanism
Tokens never reach browser JavaScripthttpOnly session cookie carries only an opaque session ID; tokens live in Redis
XSS cannot steal tokensJavaScript cannot read httpOnly cookies; no tokens in localStorage or memory
CSRF mitigatedSameSite=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 SPABFF handles refresh transparently; Angular never sees a 401 caused by token expiry
Single logout revokes all downstream accessInvalidating the BFF session in Redis immediately removes the stored tokens; no downstream service can be called without a valid session
Code interception preventedPKCE binds the authorization code to the BFF session; stolen codes are unusable without the code_verifier
client_secret never exposedSecret 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

ApproachToken StorageXSS RiskRefresh StrategyComplexity
BFF-for-SPA (this pattern)Redis (server-side)None — tokens never in browserBFF handles transparentlyHigher infrastructure; simpler Angular code
SPA + PKCE (no BFF), localStoragelocalStorageHIGH — XSS steals persistent tokensAngular must implement refresh loopLower infrastructure; higher Angular complexity + security risk
SPA + PKCE (no BFF), in-memoryJavaScript variableLOW for theft, but lost on refreshComplex — Angular must implement refresh before expiry AND on page loadNo infrastructure; highest Angular complexity
Implicit flow (deprecated)URL fragment → memoryHIGHNot 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. The SameSite cookie and Spring Security CSRF filter must both be properly configured.

ConceptRelationship
PKCEStep 2 of the flow — BFF uses PKCE to bind the authorization code to its session
OAuth2-BFF-PatternThe overarching pattern of BFF as OAuth2 confidential client
Token-Relay-PatternStep 9 of the flow — BFF forwards tokens to downstream services
Spring-Cloud-GatewayThe SCG infrastructure on which the BFF runs; provides TokenRelay= filter
BFF-PatternThe general BFF architectural pattern that makes this security model possible
  • 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)

  • 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