Expected Behavior

We need to convert additional claims (i.e. roles) from a JWT to GrantedAuthority so that we can do method-based authorization validation (i.e. @PreAuthorize). In a single tenancy, this is feasible by providing an authentication converter in SecurityWebFilterChain, i.e. something like

    final ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(new ReactiveJwtGrantedAuthoritiesConverterAdapter(new MyConverter()));
    httpSecurity.oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwt -> jwt.jwtAuthenticationConverter(converter))); 

However, it is not possible to do that when defining JwtIssuerReactiveAuthenticationManagerResolver for multi-tenancy.

Current Behavior

When JwtIssuerReactiveAuthenticationManagerResolver creates an JwtReactiveAuthenticationManager from an issuer (inside TrustedIssuerJwtAuthenticationManagerResolver), the instance is not accessible. This means that there is no easy way to override the default authentication converter (i.e. via JwtReactiveAuthenticationManager#setJwtAuthenticationConverter).

Context

The current workaround is to duplicate JwtIssuerReactiveAuthenticationManagerResolver and make it set the converter when the instance of JwtReactiveAuthenticationManager is created. This is problematic because the duplicate code can drift from this library.

Comment From: singhbaljit

I suppose we can make TrustedIssuerJwtAuthenticationManagerResolver public, and add a method to set a converter instance to applied to each new instances for the manager. Or perhaps add more fromTrustedIssuers methods to include a converter... or something else. I can make a PR, but I need to know which direction is preferred.

Comment From: jzheaux

Adding configurations like these turns out to be complex since they ultimately require a datatype like Function<String, JwtAuthenticationConverter> or a nested resolver (JwtAuthenticationConverterResolver) to work. I'd prefer to keep the API simpler than that.

I'd instead recommend providing an AuthenticationManagerResolver<String> like so:

@Component
public class MyAuthenticationManagerResolver 
        implements ReactiveAuthenticationManagerResolver<String> {
    private Map<String, ReactiveAuthenticationManager> authenticationManagers = ...;

    public Mono<ReactiveAuthenticationManager> resolve(String issuer) {
        return Mono.just(authenticationManagers.get(issuer));
    }

    public void addTrustedIssuer(String issuer) {
        ReactiveJwtDecoder decoder = ReactiveJwtDecoder.fromIssuerLocation(issuer);
        JwtReactiveAuthenticationManager jwt = new JwtReactiveAuthenticationManager(decoder);
        jwt.setJwtAuthenticationConverter(...);
        this.authenticationManagers.put(issuer, authenticationManager);
    }
}

Then you can do:

new JwtIssuerReactiveAuthenticationManagerResolver(myTrustedIssuerResolver)

Comment From: singhbaljit

That doesn't work, because the JwtReactiveAuthenticationManager needs to be resolved just-in-time when a new issuer is encountered. Basically, what TrustedIssuerJwtAuthenticationManagerResolver does. I do think that we should make TrustedIssuerJwtAuthenticationManagerResolver a stand alone public class so that this is behavior can be customized. In this case, that means providing a custom converter. In another case, it means customizing a proper cache with TTL instead of using a simple concurrent map (separate topic). We can use a builder pattern to keep APIs clean.

Comment From: singhbaljit

Using Function<String, JwtAuthenticationConverter> isn't anything new. That's exactly what's in the APIs for SecurityWebFilterChain.

This issue was closed hastily IMO.

Comment From: singhbaljit

Looks like this issue was raised before, https://github.com/spring-projects/spring-security/issues/10536.