Secure-by-default token-handler BFF for Spring WebFlux. A single auto-configuration turns a reactive application (typically a Spring Cloud Gateway) into the confidential OAuth client that runs Authorization Code + PKCE server-side, keeps the user's tokens in the server-side session, and hands the browser only an opaque
__Host-cookie — no token ever reaches the browser, and no security code is required.
- Overview
- Where it sits in the platform
- What it provides
- Key types
- Requirements
- Installation
- Quick Start
- Configuration
- Why not
security-oauth2-client - Login & relay flow
- Testing
- License
This module is the token-handler BFF binding of the Firefly hexagonal security platform. It implements the OAuth 2.0 for Browser-Based Apps token-handler pattern: the browser authenticates against a confidential server-side client, the tokens never leave the server, and the browser holds only an opaque, HttpOnly, __Host--prefixed session cookie.
It is deliberately product-agnostic and Spring-Cloud-Gateway-agnostic: it contributes only the security chain and a per-user, session-bound authorized-client manager. Routing, the TokenRelay filter and trusted-header stripping stay in the consuming application. The whole module is one @AutoConfiguration class (BffSecurityAutoConfiguration) plus a properties class. Every bean it contributes is @ConditionalOnMissingBean, so an application can override any single piece — the filter chain, the entry point, the logout handler, the authorized-client manager — without forking the module.
The security platform is layered hexagonally; this module is the edge / confidential-client binding, a sibling of the resource-server binding:
security-api → security-spi → security-core → security-webflux → security-bff → application
(ports + (driven (neutral (reactive (this module: (Spring Cloud
domain) ports) engine) Spring Security oauth2Login token- Gateway: routes +
bindings) handler chain) TokenRelay)
- This module wires Spring Security 6's reactive
oauth2Logininto the token-handler chain and exposes a session-boundReactiveOAuth2AuthorizedClientManagerso a gatewayTokenRelaycan attach the logged-in user's access token downstream. - It pairs with
security-resource-serveron the other side of the call: the BFF relays a bearer token to services that this module's resource-server sibling validates.
This module depends only on Spring Security's OAuth2 client + reactive web stack; it imports no vendor SDK and does not require Spring Cloud Gateway at compile time.
BffSecurityAutoConfiguration contributes, each gated by @ConditionalOnMissingBean:
- A token-handler
SecurityWebFilterChain(bffSecurityWebFilterChain):oauth2Loginwith forced PKCE. ADefaultServerOAuth2AuthorizationRequestResolverwithOAuth2AuthorizationRequestCustomizers.withPkce()so the authorization request always carriescode_challenge(S256) — Spring enables PKCE automatically only for public clients, but a confidential token-handler must still send it (required when the IdP enforces PKCE).- Default-deny authorization. Only the configured
permit-matchersare public; every other exchange requires authentication. - Cookie-mode CSRF.
CookieServerCsrfTokenRepository.withHttpOnlyFalse()so a SPA can read and echo theXSRF-TOKEN. - RP-initiated logout.
OidcClientInitiatedServerLogoutSuccessHandlerclears the session and redirects to the IdP end-session endpoint. - A browser-aware entry point. A document request (
Accept: text/html) is redirected to start the login (/oauth2/authorization/<registrationId>); everything else (XHR/fetch) gets a 401 so the SPA can trigger the login navigation itself.
- A session-bound authorized-client manager (
bffAuthorizedClientManager): aDefaultReactiveOAuth2AuthorizedClientManager(authorization-code + refresh-token providers) that reads the token from theServerOAuth2AuthorizedClientRepository(the WebSession, Redis-backed via Spring Session). A gatewayTokenRelayuses this bean to attach the logged-in user's access token to downstream calls — not an M2M token.
| Type | Role |
|---|---|
BffSecurityAutoConfiguration |
@AutoConfiguration entry point (@ConditionalOnWebApplication(REACTIVE), @ConditionalOnClass(ServerHttpSecurity, ReactiveClientRegistrationRepository)); builds the token-handler SecurityWebFilterChain and the session-bound authorized-client manager. |
BffSecurityProperties |
@ConfigurationProperties("firefly.security.bff") — neutral registration-id, public permit-matchers, post-logout-redirect-uri. |
The auto-configuration is registered via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
- Java 21+
- Spring Boot 3.x, Spring Security 6.x
- A reactive web stack (Spring WebFlux); typically Spring Cloud Gateway in the consuming app for routing +
TokenRelay - An OAuth2 client registration (
spring.security.oauth2.client.*) — confidential, Authorization Code - A server-side session store (Spring Session; Redis in production) to hold the tokens off the browser
- HTTPS at the edge — the
__Host-session cookie requiresSecure
The version is managed by the Firefly parent/BOM, so you can usually omit it. Depend on it directly in the BFF/gateway application:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-bff</artifactId>
</dependency>If you are not inheriting the Firefly parent, pin the version explicitly:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-bff</artifactId>
<version>26.06.03</version>
</dependency>With the module on the classpath and an OAuth2 client registration configured, the token-handler chain is active with zero security code. The consuming gateway keeps its routes and the TokenRelay + header-stripping filters:
spring:
security:
oauth2:
client:
provider:
idp:
issuer-uri: https://idp.example.com/realms/firefly
registration:
idp:
provider: idp
client-id: my-bff
client-secret: ${BFF_CLIENT_SECRET}
authorization-grant-type: authorization_code
scope: openid, profile, email
redirect-uri: "{baseUrl}/login/oauth2/code/idp"
session:
store-type: redis
cloud:
gateway:
default-filters:
- TokenRelay=
- RemoveRequestHeader=X-Tenant-Id
- RemoveRequestHeader=X-User-Id
routes:
- id: api
uri: lb://downstream-service
predicates:
- Path=/api/**
firefly:
security:
bff:
registration-id: idp
permit-matchers:
- /actuator/health/**All keys live under firefly.security.bff:
| Property | Default | Description |
|---|---|---|
registration-id |
idp |
Spring OAuth2 client registration-id. Neutral on purpose (not the provider name) so the login endpoints are /oauth2/authorization/<id> and /login/oauth2/code/<id>. |
permit-matchers |
(empty) | Ant-style paths served without authentication (e.g. actuator, docs). The OAuth2 login endpoints are handled by the login filter and need not be listed. |
post-logout-redirect-uri |
{baseUrl} |
Where to send the browser after an RP-initiated logout completes at the IdP. |
The OAuth2 client itself (provider, client-id/secret, scopes, redirect-uri) is configured with the standard spring.security.oauth2.client.* keys, and the session store with spring.session.* — this module does not duplicate them.
The framework's security-oauth2-client module is intentionally not used here. Its authorized-client manager is service/M2M-oriented (client_credentials); on a gateway it would hijack the TokenRelay and attach a service token instead of the logged-in user's token. This module fills that gap with a manager bound to the user's WebSession, which is exactly what the token-handler pattern needs.
Browser (cookie)
→ /oauth2/authorization/<id> → 302 to IdP /authorize (code_challenge S256)
→ IdP login
→ /login/oauth2/code/<id> → server-side code→token exchange
→ tokens stored in the WebSession (Spring Session / Redis); browser gets an opaque __Host- cookie
→ API call (cookie, no bearer) → TokenRelay reads the session-bound authorized client
→ adds Authorization: Bearer <user access token> downstream
An unauthenticated document request is redirected to the IdP login; an unauthenticated XHR gets 401. The browser never receives or stores a token.
The module ships a structural test, BffSecurityAutoConfigurationTest, that boots the real auto-configuration with a static (non-discovery) OAuth client registration so the context starts without a live IdP, Redis or Docker (session in-memory; Redis/Session autoconfig excluded). It asserts the secure-by-default behavior:
- the permit list is reachable without authentication;
- an unauthenticated XHR call returns 401, while a browser document request is redirected to
/oauth2/authorization/<id>; oauth2Loginis wired under the neutralregistration-idand the authorize redirect targets the IdP authorization endpoint.
Copyright 2024-2026 Firefly Software Foundation.
Licensed under the Apache License, Version 2.0. See LICENSE for details.