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);
}