I noticed that JwtIssuerAuthenticationManagerResolver works with trusted issuers, but the jwkSetUri is generated using a .well-known endpoint call. It might be worth considering creating a JwtIssuerAuthenticationManagerResolver with an issuer set and a jwkSetUri. For example:
static class TrustedIssuerAndJwkSetUriJwtAuthenticationManagerResolver implements AuthenticationManagerResolver<String> {
private final Log logger = LogFactory.getLog(getClass());
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
private final Map<String, String> trustedIssuersAndJwkSetUri;
public TrustedIssuerAndJwkSetUriJwtAuthenticationManagerResolver(Map<String, String> trustedIssuersAndJwkSetUri) {
this.trustedIssuersAndJwkSetUri = trustedIssuersAndJwkSetUri;
}
@Override
public AuthenticationManager resolve(String issuer) {
if (trustedIssuersAndJwkSetUri.containsKey(issuer)) {
String jwkSetUri = trustedIssuersAndJwkSetUri.get(issuer);
AuthenticationManager authenticationManager = this.authenticationManagers.computeIfAbsent(issuer,
(k) -> {
this.logger.debug("Constructing AuthenticationManager");
JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocationAndJwkSetUri(issuer, jwkSetUri);
return new JwtAuthenticationProvider(jwtDecoder)::authenticate;
});
this.logger.debug(LogMessage.format("Resolved AuthenticationManager for issuer '%s'", issuer));
return authenticationManager;
} else {
this.logger.debug("Did not resolve AuthenticationManager since issuer is not trusted");
}
return null;
}
}
JwtDecoders utility can be extended with the method fromIssuerLocationAndJwkSetUri:
@SuppressWarnings("unchecked")
public static <T extends JwtDecoder> T fromIssuerLocationAndJwkSetUri(String issuer, String jwkSetUri) {
NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.jwsAlgorithm(SignatureAlgorithm.RS256);
NimbusJwtDecoder jwtDecoder = builder.build();
OAuth2TokenValidator<Jwt> jwtValidator = (issuer != null)
? JwtValidators.createDefaultWithIssuer(issuer) : JwtValidators.createDefault();
jwtDecoder.setJwtValidator(jwtValidator);
return (T) jwtDecoder;
}
This solution will allow: - reduce the number of calls to the identity provider; - set non-standard jwkSetUri;
Comment From: jzheaux
Hi, @CrazyParanoid. I think adding a constructor like that might be too specific, especially given that there are myriad other ways that a JwtDecoder can be customized.
An idea that would work for more use cases than just yours would be to extract TrustedIssuerAuthenticationManagerResolver and provide a setter setJwtDecoderFactory(Converter<String, JwtDecoder>) that you could use to customize how the decoder is created. You'd use it like so:
@Bean
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver(Map<String, String> issuerToJwkSetUri) {
var factory = new JwtIssuerAuthenticationManagerFactory(issuerToJwkSetUri::containsKey);
resolver.setJwtDecoderFactory((issuer) -> {
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(issuerToJwkSetUri.get(issuer)).build();
decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
return decoder;
});
return JwtIssuerAuthenticationManagerResolver.withAuthenticationManagerFactory(factory);
}
I think a slightly better name for the extracted class is JwtIssuerAuthenticationManagerFactory and should implement Converter<String, AuthenticationManager> instead. Also, it might be nice to add setJwtAuthenticationConverterFactory since that has been a common configuration request over the years as well.
How well would this address your use case?
Comment From: franticticktick
Hi @jzheaux. Thanks for your feedback. I think I have found a solution with the current API:
@Bean
public JwtIssuerAuthenticationManagerResolver jwtIssuerAuthenticationManagerResolver(Map<String, String> issuerToJwkSetUri) {
return new JwtIssuerAuthenticationManagerResolver(
(issuer) -> {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(issuerToJwkSetUri.get(issuer)).build();
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer));
return new JwtAuthenticationProvider(jwtDecoder)::authenticate;
}
);
}
Additionally, I can implement multiple JwtAuthenticationProviders with my own JwtDecoder and add them to the ProviderManager. It seems that this solution is more flexible.