Context:
I'm working with multiple OAuth2 Clients where I need to validate JWT tokens (id token) which is using JwtDecoderFactory<C>. The default implementation is OidcIdTokenDecoderFactory, which offers methods like setJwtValidatorFactory, setJwsAlgorithmResolver, and setClaimTypeConverterFactory. However, I believe there isn't a method to directly set a decoder for a specific client, which poses a challenge when some client registrations have JWKS URIs while others have a public key available in the classpath.
Issue:
I need to configure different decoders for different client registrations, but the existing OidcIdTokenDecoderFactory doesn't provide a straightforward way to set the decoder for each client registration.
Question:
Is it acceptable to create a new method to set a decoder specifically for different client registrations within OidcIdTokenDecoderFactory? If so, what would be the best approach to implement this without causing unintended side effects or breaking existing functionality?
Any insights or examples on extending OidcIdTokenDecoderFactory to accommodate per-client registration decoders would be greatly appreciated.
Comment From: franticticktick
Hi @kcsurapaneni! I see that OidcIdTokenDecoderFactory contains jwtDecoders. If the ClientRegistrationRepository contains several clients, then the jwtDecoders variable will contain the required jwtDecoder:
@Override
public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), (key) -> {
NimbusJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(clientRegistration));
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter = this.claimTypeConverterFactory
.apply(clientRegistration);
if (claimTypeConverter != null) {
jwtDecoder.setClaimSetConverter(claimTypeConverter);
}
return jwtDecoder;
});
}
Each clientRegistration will have its own jwtDecoder called. There is another problem here - OidcIdTokenDecoderFactory will apply the same settings to different ClientRegistrations.
Comment From: kcsurapaneni
Hi @CrazyParanoid, thank you for your response! I’m not quite sure I understand your point. The code snippet you shared is from the OidcIdTokenDecoderFactory implementation.
If the
ClientRegistrationRepositorycontains several clients, then thejwtDecodersvariable will contain the required jwtDecoder.
That’s true, but I believe these are built solely using jwks_uri. In the snippet you provided, it first checks for availability and, if not found, calls the buildDecoder method, which relies only on jwks_uri.
There is another problem here -
OidcIdTokenDecoderFactorywill apply the same settings to differentClientRegistrations.
I think this can be resolved with the available set* methods, allowing us to customize things like ClaimTypeConverter, etc.
Is there a way we could create our own jwtDecoder at the time of clientRegistration without using jwks_uri?
Comment From: franticticktick
You can try to implement your own JwtDecoderFactory and add it to OidcAuthorizationCodeAuthenticationProvider via setJwtDecoderFactory. There is no way to override this logic in OidcIdTokenDecoderFactory. It will need some modification, something like:
private Function<ClientRegistration, JwtDecoder> jwtDecoderFactory = new DefaultJwtDecoderFactory();
@Override
public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(),
(key) -> this.jwtDecoderFactory.apply(clientRegistration));
}
With setJwtDecoderFactory method.
Comment From: kcsurapaneni
I did something like the following, but it seems not great, as it is tightly coupled with client registration id and also missing the other implementations (like ClaimTypeConverter) provided by OidcIdTokenDecoderFactory.
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
return clientRegistration -> {
if ("xxxxxxxxx".equals(clientRegistration.getRegistrationId())) {
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
return NimbusJwtDecoder.withJwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri()).build();
};
}
Comment From: sjohnr
Any insights or examples on extending
OidcIdTokenDecoderFactoryto accommodate per-client registration decoders would be greatly appreciated.
Thanks for getting in touch @kcsurapaneni, but it feels like this is a question that would be better suited to Stack Overflow. We prefer to use GitHub issues only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add a minimal sample that reproduces this issue if you feel this is a genuine bug.
Having said that, I want to make sure I'm understanding your challenge.
I did something like the following, but it seems not great, as it is tightly coupled with client registration id
You aren't limited to just checking against a hard-coded String, you can do anything needed here to determine whether to use publicKey. For example, you could check whether jwkSetUri is null (see example below) or even query a database, etc. I believe that you aren't too limited here. Please let me know if I'm misunderstanding something on this point.
and also missing the other implementations (like
ClaimTypeConverter) provided byOidcIdTokenDecoderFactory.
You can re-use OidcIdTokenDecoderFactory using delegation and/or obtain ClaimTypeConverter from OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter(), like so:
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
OidcIdTokenDecoderFactory delegate = new OidcIdTokenDecoderFactory();
// TODO: Configure OidcIdTokenDecoderFactory if needed...
Map<ClientRegistration, JwtDecoder> publicKeyJwtDecoders = new ConcurrentHashMap<>();
return (clientRegistration) -> {
if (clientRegistration.getProviderDetails().getJwkSetUri() != null) {
return delegate.createDecoder(clientRegistration);
}
return publicKeyJwtDecoders.computeIfAbsent(clientRegistration, (key) -> {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey).build();
jwtDecoder.setClaimSetConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter());
return jwtDecoder;
});
};
}
If I'm misunderstanding anything, please let me know. Otherwise, I plan on closing this issue with the above suggestions to move you forward.
Comment From: kcsurapaneni
@sjohnr Thank you for your response.
it feels like this is a question that would be better suited to Stack Overflow. We prefer to use GitHub issues only for bugs and enhancements.
Initially, I also considered Stack Overflow. However, upon further consideration, I realized it could be an enhancement request, which is why I created a ticket here. After reviewing your response, I now understand that it is indeed more appropriate for Stack Overflow. I apologize for any confusion caused by my misunderstanding.
My original question was about creating a JWT decoder when the jwks_url is using a classpath (like classpath:/keys/oauth-public.key) value. Your solution provided me with a much better understanding, and I have managed to implement a solution similar to yours. Here's the revised code I came up with:
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
return clientRegistration -> {
OidcIdTokenDecoderFactory delegate = new OidcIdTokenDecoderFactory();
if (StringUtils.hasLength(clientRegistration.getProviderDetails().getJwkSetUri()) &&
clientRegistration.getProviderDetails().getJwkSetUri().startsWith("classpath")) { // This checks whether jwks uri is associated with classpath or not?
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(clientRegistration.getProviderDetails().getJwkSetUri()).build(); // here actually converting the classpath string to RSAPublicKey
jwtDecoder.setClaimSetConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter());
return jwtDecoder;
}
return delegate.createDecoder(clientRegistration);
};
}
This solution resolved my issue, so I will be closing this discussion with this comment. Additionally, I will update the title to "How can classpath public key values be utilized in the OAuth2 client jwks_uri?"