I need to implement a kinda "bff" but with oauth2 authorisation code grant. Like, client requests a rest endpoint from "bff", "bff" acts like a resource server; then bff queries rest endpoints from multiple services with feign. These services act as a resource servers for this "bff". Then, "bff" should process the responses, making its own response, and send it to the client then. Something like "Authorization" header forwarding, I suppose. But have no idea how to implement it. Maybe something like spring cloud gateway but a bit more intelligent. Or, maybe it is possible to implement a filter that will just relay this header?
Any documentation or better small but complete code sample would be just great. Thank you!
What you are looking for is probably a RequestInterceptor
. In a Spring boot app, exposing one as a @Component
or @Bean
(it is a functional interface you can implement with a lambda) should be enough.
This could be implemented:
@Bean
RequestInterceptor resourceServerBearerRequestInterceptor() {
return (RequestTemplate template) -> {
final var auth = SecurityContextHolder.getContext().getAuthentication();
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes servletRequestAttributes) {
if (auth instanceof AbstractOAuth2TokenAuthenticationToken oauth) {
template.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(oauth.getToken().getTokenValue()));
}
}
};
}
oauth2Login
@Component
@RequiredArgsConstructor
public class ClientBearerRequestInterceptor implements RequestInterceptor {
private final OAuth2AuthorizedClientRepository authorizedClientRepo;
public void apply(RequestTemplate template) {
final var auth = SecurityContextHolder.getContext().getAuthentication();
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes servletRequestAttributes) {
if (auth instanceof OAuth2AuthenticationToken oauth) {
final var authorizedClient = authorizedClientRepo.loadAuthorizedClient(oauth.getAuthorizedClientRegistrationId(), auth, servletRequestAttributes.getRequest());
if (authorizedClient != null) {
template.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(authorizedClient.getAccessToken().getTokenValue()));
}
}
}
}
}
If your app is not a Boot app (or if you define your RequestInterceptor
out of @ComponentScan
), you could have to provide the configuration class as @FeignClient(configuration = ...)
The pattern in the question drawing is now discouraged for security reasons:
As defined in an ITEF document linked by the author of the question in the comments a "Full BFF" is an acronym for backends configured as OAuth2 clients bridging between session authorization (for the frontends) and Bearer
header authorization for the requests it forwards to resource servers. However, the pattern exposed in this document diverges from the Full BFF: the OAuth2 client (what retrieves OAuth2 tokens from the authorization server) is on the backend but the BFF exposes access tokens to the frontend so that it can call resource servers directly and session authorization is used only to retrieve access tokens (motivation for this quite unusual pattern is performance, apparently).
To my knowledge, Spring Security team recommendations is to hide tokens from mobile or Javascript based web apps, and to have it query OAuth2 clients using sessions. Also, all major web apps I know (Google, Facebook, Github, Stackoverflow, Linkedin, and so many more) apply this pattern: inspect those web apps with your browser debugging tools, you won't find a single access token (just session cookies).
spring-cloud-gateway
is a perfect fit to implement a "Full BFF" when configured with spring-boot-starter-oauth2-client
(with oauth2Login
and CSRF protection), and TokenRelay=
filter (this filter replaces the session cookie with the access token in session before forwarding a request). Also, it scales very well when used with Spring Session, which removes the limitation motivating the usage of a "non Full" BFF as exposed in the ITEF document linked above.
I have written a Full BFF tutorial for Baeldung, but be aware that it is using an additional starter of mine to ease security configuration and that you should probably read pro and cons before using such a lib (this latest link also contains references to instructions for writing security conf without my starter).
To implement the pattern described in the ITEF document linked by the question author, where the BFF fetches and stores tokens but exposes access tokens to frontends so that it can call resource servers directly, the BFF app configured as both:
oauth2Login
to expose an endpoint retrieving the access token value from the OAuth2AuthorizedClientRepository
, using the OAuth2AuthenticationToken
in the security contextFor that, you'll need two SecurityFilterChain
beans: one with OAuth2 client conf for login and the access-token endpoint, and another one with OAuth2 resource server conf for REST endpoints. This filter-chains beans will need to have
@Order
@Order
must define a securityMatcher
to define which requests it should processFor a servlet app and using a starter of mine to ease security config, this would give something like:
@Component
public class ResourceServerBearerRequestInterceptor implements RequestInterceptor {
public void apply(RequestTemplate template) {
final var auth = SecurityContextHolder.getContext().getAuthentication();
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes servletRequestAttributes) {
if (auth instanceof AbstractOAuth2TokenAuthenticationToken oauth) {
template.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(oauth.getToken().getTokenValue()));
}
}
}
}
@FeignClient(name = "quizzes", url = "${spring.cloud.openfeign.client.quizzes.url}")
public interface QuizClient {
@RequestMapping(method = RequestMethod.GET, value = "/quizzes")
List<QuizDto> getAllQuizzes();
}
@RestController
@RequiredArgsConstructor
@Validated
@Tag(name = "QuizProxy")
public class QuizProxyController {
private final QuizClient quizzes;
@GetMapping("/proxy/quizzes")
public Integer getAccessToken() {
return quizzes.getAllQuizzes().size();
}
}
@RestController
@RequiredArgsConstructor
@Validated
@Tag(name = "AccessToken")
public class AccessTokenController {
private final OAuth2AuthorizedClientRepository authorizedClientRepo;
@GetMapping("/access-token")
@PreAuthorize("isAuthenticated()")
public AccessTokenDto getAccessToken(OAuth2AuthenticationToken auth, HttpServletRequest request) {
final var authorizedClient = authorizedClientRepo.loadAuthorizedClient(auth.getAuthorizedClientRegistrationId(), auth, request);
return authorizedClient == null
? null
: new AccessTokenDto(authorizedClient.getAccessToken().getTokenValue(), authorizedClient.getAccessToken().getExpiresAt().getEpochSecond());
}
public static record AccessTokenDto(String accessToken, long expiry) {
}
}
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.1.7</version>
</dependency>
scheme: http
oauth2-issuer: https://oidc.c4-soft.com/auth/realms/quiz
oauth2-client-id: quiz-bff
oauth2-client-secret: change-me
frontend: ${scheme}://localhost:4200
quizzes-resources: ${scheme}://localhost:7084
server:
port: 8080
error:
include-message: always
shutdown: graceful
ssl:
enabled: false
spring:
config:
import:
- optional:configtree:/workspace/config/
- optional:configtree:/workspace/secret/
lifecycle:
timeout-per-shutdown-phase: 30s
cloud:
openfeign:
client:
quizzes:
url: ${quizzes-resources}
security:
oauth2:
client:
provider:
c4:
issuer-uri: ${oauth2-issuer}
registration:
c4:
provider: c4
authorization-grant-type: authoriztion_code
client-id: ${OAuth2-client-ID}
client-secret: ${OAuth2-client-secret}
scope:
- openid
- profile
- email
- offline_access
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: ${oauth2-issuer}
username-claim: preferred_username
authorities:
- path: $.resource_access.${oauth2-client-id}.roles
client:
security-matchers:
- /login/**
- /oauth2/**
- /access-token
permit-all:
- /login/**
- /oauth2/**
cors:
- path: /**
allowed-origin-patterns:
- ${frontend}
post-login-redirect-host: ${frontend}
post-login-redirect-path: /
post-logout-redirect-host: ${frontend}
post-logout-redirect-path: /
resourceserver:
permit-all:
- "/proxy/**"
- "/actuator/health/readiness"
- "/actuator/health/liveness"
- "/v3/api-docs/**"
cors:
- path: /**
allowed-origin-patterns:
- ${frontend}
management:
endpoint:
health:
probes:
enabled: true
endpoints:
web:
exposure:
include: '*'
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
logging:
level:
org:
springframework:
security: TRACE
---
server:
ssl:
enabled: true
spring:
config:
activate:
on-profile: ssl
Please note that in addition to adding quite some complexity to both front and back ends (compared to Full BFF), such a solution solves only some of known vulnerabilities for frontends configured as "public" clients:
But access tokens are still exposed to Javascript or mobile apps code.
Considering how well spring-cloud-gateway
scales with Spring session, I'm personally more inclined to implement "Full BFF".