Reputation: 21
We need to build a service that supports both legacy bespoke (not JWT) Bearer tokens Auth Headers:
Authorization: bespoke ....
and JWT Bearer token Auth headers:
Authorization: Bearer ......
What would be the cleanest way of implementing a Bean that composes both Bespoke + JWT validation as fallback in Spring Boot 3.2 using Webflux and oauth2-resource-server?
I expect a custom bean tha composes DefaultBearerTokenResolver and a bespoke resolver, or a custom ReactiveAuthenticationManagerResolver bean that does the same thing, but I'm not sure what would be simpler.
Upvotes: 1
Views: 334
Reputation: 12825
If the security configuration is sensibly different between the two, you should define different SecurityWebFilterChain
beans:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityWebFilterChain bespokeFilterChain(ServerHttpSecurity http) {
http.securityMatcher((ServerWebExchange exchange) -> {
final var isBespoke = Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)).map(auth -> auth.toLowerCase().startsWith("bespoke ") ? true : false).orElse(false);
return isBespoke ? ServerWebExchangeMatcher.MatchResult.match() : ServerWebExchangeMatcher.MatchResult.notMatch();
});
...
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
SecurityWebFilterChain bearerFilterChain(ServerHttpSecurity http) {
http.securityMatcher((ServerWebExchange exchange) -> {
final var isBearer = Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)).map(auth -> auth.toLowerCase().startsWith("bearer ") ? true : false).orElse(false);
return isBearer ? ServerWebExchangeMatcher.MatchResult.match() : ServerWebExchangeMatcher.MatchResult.notMatch();
});
...
}
...
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
SecurityWebFilterChain defaultFilterChain(ServerHttpSecurity http) {
// No http.securityMatcher here
// Processes requests which were not matched in filter chains with higher precedence (for instance anonymous requests)
...
}
Here I demo matchers on Authorization
header, but you can match on anything from the request (path, media-types, origin IP, etc.).
This is very useful when the security requirements are different between the different authentication strategies like, for instance, when mixing in a single application:
oauth2Login()
: requires sessions, CSRF protection and generally configured with 302 redirect to login
for unauthorized requests to protected resourcesoauth2ResourceServer()
: generally stateless (neither session nor CSRF protection) with 401 unauthorized
If everything but the authentication manager is the same, then instead of writing mostly identical filter-chain beans, you should probably implement your own ReactiveAuthenticationManagerResolver<ServerWebExchange>
:
@Bean
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, ReactiveAuthenticationManagerResolver<ServerWebExchange> authenticationManagerResolver) {
http.oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));
...
}
@Component
static class MyReactiveAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
private final ReactiveAuthenticationManager bespokeAuthManager;
private final ReactiveAuthenticationManager bearerAuthManager;
private final ReactiveAuthenticationManager defaultAuthManager;
MyReactiveAuthenticationManagerResolver() {
...
}
@Override
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange context) {
final var authHeader = Optional.ofNullable(context.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)).map(auth -> auth.toLowerCase()).orElse("");
if(authHeader.startsWith("bespoke ")) {
return Mono.just(bespokeAuthManager);
}
if(authHeader.startsWith("bearer ")) {
return Mono.just(bearerAuthManager);
}
return Mono.just(defaultAuthManager);
}
}
Upvotes: 1