DLL
DLL

Reputation: 21

How to support different Bearer token types in Spring Boot?

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

Answers (1)

ch4mp
ch4mp

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 resources
  • oauth2ResourceServer(): 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

Related Questions