OAuth2 BFF Pattern
OAuth2 BFF Pattern
The architectural pattern in which the BFF acts as the OAuth2 confidential client — acquiring, storing, and managing tokens entirely server-side — so that browser-based clients never handle OAuth2 tokens directly.
Core Idea
A browser-based application (Angular, React, Vue) cannot safely hold a client_secret. Storing tokens in localStorage exposes them to XSS. Storing tokens in JavaScript memory means they vanish on page reload. The OAuth2 BFF Pattern solves this by making the BFF the sole OAuth2 client:
- BFF has the
client_secret— securely held in server-side environment variables - BFF performs the Authorization Code Flow — including PKCE for all clients (OAuth 2.1 mandate)
- BFF stores
access_token+refresh_tokenin a server-side session — the browser receives only an opaque,httpOnly,Securesession cookie - BFF silently refreshes expired tokens — the Angular app is unaware; it simply keeps calling the BFF API
- Angular never touches OAuth2 tokens — no tokens in
localStorage,sessionStorage, or JavaScript variables
This is the pattern recommended by RFC 9700 (OAuth 2.0 Security BCP), Philippe De Ryck, Okta, and the broader security community as of 2024+.
Key Principles
- Confidential client, server-side only — the BFF registers as a confidential client with the IdP; only confidential clients may hold a
client_secret - Session = token store —
access_tokenandrefresh_tokenlive in Redis-backed server-side session, never in the browser - httpOnly + Secure + SameSite cookie — the session identifier sent to the browser is protected from JavaScript access and CSRF
- Transparent to Angular — the Angular app makes ordinary HTTP calls to the BFF; token management is completely invisible
- PKCE is mandatory — even for confidential BFF clients, per RFC 9700
How It Works
Token Acquisition: Authorization Code Flow
Angular BFF IdP (Keycloak)
| | |
|-- GET /protected --->| |
| |-- is authenticated? No |
|<-- 302 /oauth2/auth--| |
| | |
| (browser follows redirect automatically) |
|-- GET /oauth2/authorization/keycloak ----------->|
| | |
| BFF generates PKCE code_verifier, stores in session
| BFF redirects to IdP with code_challenge |
|<-- 302 to IdP /authorize ----------------------->|
| |
|------ (user authenticates at IdP) -------------->|
| |
|<-- 302 /login/oauth2/code/keycloak?code=XYZ -----|
|-- GET /login/oauth2/code/keycloak?code=XYZ ----->|
| | |
| BFF exchanges code + code_verifier |
| |--- POST /token ---------->|
| |<-- {access_token, ..} ----|
| | |
| BFF stores tokens in Redis session |
| BFF sets httpOnly session cookie |
|<-- 302 to original destination + Set-Cookie -----|
| | |
|-- GET /api/dashboard (with session cookie) ----->|
| | |
| BFF retrieves token from session |
| BFF calls downstream with Bearer token |
|<-- 200 dashboard data ---------------------------|
Spring Boot Configuration for Token Storage
// Dependencies required:
// spring-boot-starter-oauth2-client
// spring-session-data-redis
// spring-boot-starter-data-redis-reactive
@Configuration
@EnableWebFluxSecurity
public class BffSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/actuator/health", "/actuator/info").permitAll()
.pathMatchers("/oauth2/**", "/login/**", "/logout").permitAll()
.anyExchange().authenticated()
)
.oauth2Login(Customizer.withDefaults()) // enables Authorization Code Flow with PKCE
.csrf(csrf -> csrf
// Enable CSRF for browser-facing BFF — do NOT disable
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
// Angular reads the XSRF-TOKEN cookie and sends X-XSRF-TOKEN header
)
.logout(logout -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
)
.build();
}
}Redis Session Configuration
spring:
session:
store-type: redis
timeout: 30m
redis:
namespace: bff:session
flush-mode: on-save
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
ssl:
enabled: ${REDIS_SSL:false}The ReactiveClientRegistrationRepository and ServerOAuth2AuthorizedClientRepository beans are auto-configured by Spring Boot when spring-boot-starter-oauth2-client is on the classpath. Spring Session (with Redis) persists these across BFF instances, enabling horizontal scaling.
Token Refresh
When the access_token expires, the BFF automatically refreshes it using the stored refresh_token. This is handled by Spring Security's ReactiveOAuth2AuthorizedClientManager when offline_access scope is requested and the IdP returns a refresh_token.
The Angular app simply retries (or continues) its request — it sees no token expiry. The BFF transparently:
- Detects the expired token (via a 401 from the downstream service, or by checking
expires_at) - Uses the
refresh_tokento obtain a newaccess_tokenfrom the IdP - Stores the new token pair in Redis
- Retries the downstream call
Security Properties
| Property | Mechanism |
|---|---|
| No tokens in browser | Tokens stored in Redis session; browser holds only session cookie |
| XSS cannot steal tokens | httpOnly cookie — JavaScript cannot read it |
| CSRF protection | SameSite=Lax cookie + CSRF token for state-changing requests |
| Token confidentiality | client_secret never sent to browser; held only in BFF server memory / env vars |
| Code interception prevention | PKCE (see PKCE) binds code to BFF session |
| Token replay prevention | Short-lived access_token + refresh_token rotation on each refresh |
Common Misconceptions
"Store the JWT in localStorage for simplicity"— localStorage is readable by any JavaScript on the page; a single XSS vulnerability compromises all user tokens permanently."The BFF pattern is overkill; I'll use a public client instead"— Public clients cannot hold aclient_secretand are limited in what they can do securely. The BFF confidential client pattern is specifically designed for browser-first applications."OAuth2 implicit flow solves this without a BFF"— Implicit flow was deprecated in OAuth 2.1. It returned tokens directly to the browser via URL fragments — worse than Authorization Code Flow."SPA can use Authorization Code Flow directly with PKCE and no BFF"— This is technically possible but means the SPA must store tokens somewhere (localStorage = XSS risk, memory = lost on reload). The BFF pattern eliminates this trade-off entirely.
Why It Matters
The OAuth2 BFF Pattern is the current best-practice answer to "how does a modern SPA authenticate securely?" It supersedes all previous patterns (implicit flow, SPA + PKCE without BFF, localStorage token storage). The BFF's server-side position is precisely what enables this security model — see BFF-Pattern for why the BFF exists in the first place.
Related Concepts
| Concept | Relationship |
|---|---|
| BFF-For-SPA | The full SPA-specific flow that uses this pattern end-to-end |
| PKCE | Required security extension used during Authorization Code Flow |
| Token-Relay-Pattern | How BFF forwards acquired tokens to downstream services |
| BFF-Pattern | The server-side intermediary that makes this pattern possible |
| Spring-Cloud-Gateway | The SCG infrastructure on which the BFF OAuth2 client typically runs |
Related Security Patterns
- OAuth2-OIDC-Flows — generalises the OAuth2 grant types (Authorization Code, Client Credentials, Device) beyond the BFF context; the BFF pattern uses Authorization Code + PKCE from this taxonomy
- JWT — the token format carried through the BFF; JWT validation order (signature before claims) and algorithm selection apply to every token the BFF relays
- Session-Management — the BFF stores tokens server-side in Redis behind an httpOnly cookie; Session-Management covers the session lifecycle (creation, rotation, fixation prevention) that protects this storage
Sources
- RFC 9700 — OAuth 2.0 Security Best Current Practice (tools.ietf.org/html/rfc9700)
- RFC 6749 — The OAuth 2.0 Authorization Framework
- Philippe De Ryck, "The BFF Pattern" — pragmaticwebsecurity.com
- Spring Security Reference: OAuth2 Login (docs.spring.io/spring-security/reference/reactive/oauth2/login)
- Okta Developer Blog, "Is the OAuth 2.0 Implicit Flow Dead?"
Backlinks
- BFF-Pattern — BFF's server-side position enables OAuth2 confidential client role
- BFF-For-SPA — end-to-end SPA security flow using this pattern
- P4-BFF-Security — Phase 4 research note covering BFF security patterns