This issue is related to @jzheaux as he requested some of the changes from PR #9005 to be moved to a separate ticket. On-hold until 9005 is merged.

Expected Behavior

It should be possible to override / customize JwtAuthenticationProvider inside JwtIssuerAuthenticationManagerResolver class, in a multi-tenant environment, so that the end-user can set non-standard behavior that may be desired (for example, custom JWT parsing).

Current Behavior

Customization is not possible at all, end-user is forced to use predefined implementations inside JwtIssuerAuthenticationManagerResolver, and this leads to errors if JWTs contain something uncommon. Context

Using an external oauth2ResourceServer, in a multi-tenant environment, it should be possible to override / select a custom JwtAuthenticationProvider with a custom JwtAuthenticationConverter, as not all of the JWT tokens are the same, and this leads to errors.

Please also see #9005 and #8535 for more information and the timeline of changes.

Comment From: jzheaux

Thanks for logging this ticket, @AbstractConcept

I agree that customizing how the underlying AuthenticationManager is resolved may be valuable. But, I think that exposing JwtAuthenticationConverter is too narrow. What if I need to customize the way the JwtDecoder is constructed?

Instead of taking a JwtAuthenticationConverter in the constructor, it would address more use cases to take an AuthenticationManagerResolver<String>.

So, instead of

new JwtIssuerAuthenticationManagerResolver(myTrustedIssuers,
    myAuthenticationConverter)

you'd do:

new JwtIssuerAuthenticationManagerResolver(myTrustedIssuers,
    (issuer) -> {
        JwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer);
        JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder);
        provider.setJwtAuthenticationConverter(myAuthenticationConverter);
        return provider::authenticate;
    });

While slightly more verbose, it allows applications to specify a wider range of customizations.

I think it would also be good to adjust a parameter name in another constructor, giving this class four constructors like so:

(String... trustedIssuers)
(Collection<String> trustedIssuers)
(Collection<String> trustedIssuers, AuthenticationManagerResolver<String> issuerAuthenticationManagerResolver)
(AuthenticationManagerResolver<String> trustedIssuerAuthenticationManagerResolver)

Comment From: AbstractConcept

@jzheaux Should this one still be completed, or did #9168 also took care of this?

Comment From: jzheaux

9168 did not take care of this ticket, thanks for double-checking.

Comment From: AbstractConcept

I should be available to complete this one then, but if someone can do it faster, by all means, go ahead.

Comment From: fabiangr

This affects a project I'm currently working on. We need both a custom JWTConverter and multi-tenancy. Our workaround is a configuration that looks like this:

public OAuth2SecurityConfiguration(@Value("${security.issuer-uris}") String issuerUris) {
  String[] trustedIssuers = issuerUris.split(",");
  OAuth2JwtAuthenticationConverter jwtAuthenticationConverter = new OAuth2JwtAuthenticationConverter();
  Arrays.stream(trustedIssuers).forEach(issuer -> {
    JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(JwtDecoders.fromOidcIssuerLocation(issuer));
    authenticationProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
    authenticationManagers.put(issuer, authenticationProvider::authenticate);
  });
}

Is this issue still going to be adressed?

Comment From: jzheaux

Thanks for checking, @fabiangr. I think it's open for someone with time to contribute a PR.

@AbstractConcept, is this something you are still able to contribute?

Comment From: cselagea

@jzheaux @fabiangr @AbstractConcept I didn't see any work in progress on this, and I find it useful as well, so I opened a PR.

Comment From: jzheaux

After reviewing this in conjunction with #10002, I don't think we want to add more constructors to JwtIssuerAuthenticationManagerResolver.

Instead, please consider a custom resolver like so:

@Component
public class MyAuthenticationManagerResolver implements AuthenticationManagerResolver<String> {
  private final Collection<String> validIssuers;

  // ...

  @Override
  @Cacheable(unless="#result==null")
  public AuthenticationManager resolve(String issuer) {
    if (!this.validIssuers.contains(issuer)) {
      return null;
    }
    JwtAuthenticationProvider provider = new JwtAuthenticationProvider(
            JwtDecoders.fromIssuerLocation(issuer));
    provider.setAuthenticationConverter(myConverter);
    return provider::authenticate;
  }
}

// ...

@Bean 
JwtIssuerAuthenticationManagerResolver multitenancy(
        AuthenticationManagerResolver<String> resolver) {
  return new JwtIssuerAuthenticationManagerResolver(resolver);
}

Or, with the introduction of SupplierJwtDecoder, computation can be deferred a bit more simply:

@Bean 
JwtIssuerAuthenticationManagerResolver byIssuer(MyJwtConverter converter) {
    Map<String, AuthenticationManager> managers = new HashMap<>();
    for (String issuer : issuers) {
        JwtDecoder decoder = new SupplierJwtDecoder(() -> JwtDecoders.fromIssuerLocation(issuer));
        JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder);
        provider.setJwtAuthenticationConverter(converter);
        managers.put(issuer, provider::authenticate);
    }
    return new JwtIssuerAuthenticationManagerResolver(managers::get);
}