Search code examples
spring-bootjwtspring-webflux

Spring Webflux Security Filterchain JwtIssuerReactiveAuthenticationManagerResolver with jwt converter


I have a simple Security filter chain configured for multitenancy. However, I cannot add my customer jwtConverter.

Here is the setup

 @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,  Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter) {
        JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver =
                JwtIssuerReactiveAuthenticationManagerResolver
                        .fromTrustedIssuers("issuer1", "issuer2");

        http
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(spec -> spec
                        .pathMatchers(
                                "/api/v1/profile/public/**",
                                "/api/docs/**",
                                "/swagger/**"
                        ).permitAll()
                        .anyExchange()
                        .authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));
        return http.build();
    }

When I add jwtConverter

 .oauth2ResourceServer(oauth2 ->
                        oauth2
                                .jwt(jwtSpec -> jwtSpec.jwtAuthenticationConverter(jwtAuthenticationConverter))
                                .authenticationManagerResolver(authenticationManagerResolver)

                )

I'm getting an error If an authenticationManagerResolver() is configured, then it takes precedence over any jwt() or opaqueToken() configuration

How do I resolve this in spring webflux 3.2.1?


Solution

  • Well, after a bit of cross checking on past raised github issues on the same, I've managed to create a custom JwtIssuerReactiveAuthenticationManagerResolver with a CustomAuthenticationManager that has authenticate method which can be extended to allow a customAuthentication converter.

    Here is my solution

    Simple CustomAuthenticationConverter

    public class CustomAuthenticationConverter {
        private static final String ROLES = "roleName";
        private static final String ROLE_PREFIX = "ROLE_";
    
        public Mono<Authentication> convertJwtToAuthentication(Jwt jwt) {
            Collection<GrantedAuthority> authorities = extractAuthoritiesFromJwt(jwt);
            return Mono.just(new JwtAuthenticationToken(jwt, authorities, jwt.getClaimAsString("userPublicId")));
        }
    
        private Collection<GrantedAuthority> extractAuthoritiesFromJwt(Jwt jwt) {
            var realmRoles = realmRoles(jwt);
    
            return realmRoles
                    .stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toSet());
        }
    
        private List<String> realmRoles(Jwt jwt) {
            return Optional.of(ROLE_PREFIX.concat(jwt.getClaimAsString(ROLES)))
                    .map(List::of)
                    .orElse(emptyList());
        }
    }
    

    CustomAuthenticationManager

    public class CustomAuthenticationManager implements ReactiveAuthenticationManager {
    
        private final ReactiveJwtDecoder decoder;
    
        public CustomAuthenticationManager(ReactiveJwtDecoder decoder) {
            this.decoder = decoder;
        }
    
        @Override
        public Mono<Authentication> authenticate(Authentication authentication) {
            // Implement custom authentication logic here
            BearerTokenAuthenticationToken token = (BearerTokenAuthenticationToken) authentication;
            Mono<Jwt> jwtMono = decoder.decode(token.getToken());
    
            return jwtMono.flatMap(jwt -> new CustomAuthenticationConverter().convertJwtToAuthentication(jwt));
        }
    }
    

    JwtIssuerReactiveAuthenticationManagerResolver

    @Bean
        public JwtIssuerReactiveAuthenticationManagerResolver jwtIssuerReactiveAuthenticationManagerResolver() {
            Map<String, ReactiveAuthenticationManager> managers = new HashMap<>();
            for (String issuer : trustedIssuers) {
                ReactiveJwtDecoder decoder = ReactiveJwtDecoders.fromIssuerLocation(issuer);
                managers.put(issuer, new CustomAuthenticationManager(decoder));
            }
            return new JwtIssuerReactiveAuthenticationManagerResolver(issuer -> Mono.justOrEmpty(managers.get(issuer)));
        }
    

    The add the resolver in SecurityWebFilterChain

    .oauth2ResourceServer(oauth2 -> oauth2.authenticationManagerResolver(jwtIssuerReactiveAuthenticationManagerResolver()));
    

    And ofcourse you'd need to have a list of trustedIssuers.