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.