The new 5.3.0 release includes the very useful Boot property for setting a specific JWT key in public-key-location. However, this has a hard-coded assumption of an X509-format pubkey. The jwk-set-uri property points to a JWK Set, but it is unconditionally handed off to a RestTemplate for resolution.
It would be helpful if the JWK Set could be specified using classpath:my.jwk so that for development or testing a local JWK JSON object could be used out of local resources. The logic in NimbusJwtDecoder (particularly around RestOperationsResourceRetriever) looks like it may be adaptable to supporting Resource paths, or conversely the Resource#toURL() could be processed at launch and fed to it.
Does either of these approaches sound like a practical option for allowing "canned" JWK use for testing?
Comment From: jzheaux
@chrylis it's an interesting idea, thanks for sharing
It would be helpful if the JWK Set could be specified using classpath:my.jwk so that for development or testing a local JWK JSON object could be used out of local resources.
I'm a little hesitant to add something to the public API that would only be for testing or for local use.
For testing, it seems like it would be sufficient to mock the JwtDecoder or the RestOperations.
For development, you can use new NimbusJwtDecoder(JWTProcessor), probably providing an ImmutableJWKSet instance when configuring the ConfigurableJWTProcessor.
The logic in NimbusJwtDecoder (particularly around RestOperationsResourceRetriever) looks like it may be adaptable to supporting Resource paths, or conversely the Resource#toURL() could be processed at launch and fed to it.
RestOperationsResourceRetriever is intended for use with a RestOperations whose primary implementation, RestTemplate, states:
Synchronous client to perform HTTP requests
So, I feel like adapting that to pull from a file system would be a little confusing for developers.
Potentially, something like NimbusJwtDecoder.withJwkSetLocation and a separate builder could be added, though, again I'd prefer not to add it until there is a clear production need.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.
Comment From: bossqone
I was able to solve this issue with tips mentioned above. Maybe it will be useful for someone else as well.
@Bean
public JwtDecoder jwtDecoder(final OAuth2ResourceServerProperties properties, final ResourceLoader resourceLoader) throws Exception {
// this workaround is needed because spring boot doesn't support loading jwks from classpath/file (only http/https)
// using standard 'spring.security.oauth2.resourceserver.jwt.jwk-set-uri' configuration property
// relevant issue: https://github.com/spring-projects/spring-security/issues/8092
final String issuerUri = properties.getJwt().getIssuerUri();
final String jwkSetUri = properties.getJwt().getJwkSetUri();
final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(properties.getJwt().getJwsAlgorithm());
final InputStream inputStream = resourceLoader.getResource(jwkSetUri).getInputStream();
final JWKSet jwkSet = JWKSet.load(inputStream);
final JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(jwkSet);
final ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
final JWSKeySelector<SecurityContext> jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgorithm, jwkSource);
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Spring Security validates the claim set independent from Nimbus
// copied from 'org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder.processor'
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
final OAuth2TokenValidator<Jwt> defaultValidator = JwtValidators.createDefaultWithIssuer(issuerUri);
final OAuth2TokenValidator<Jwt> clientIdValidator = new JwtClaimValidator<String>(CLIENT_ID_CLAIM, value -> !value.trim().isEmpty());
final OAuth2TokenValidator<Jwt> combinedValidator = new DelegatingOAuth2TokenValidator<>(defaultValidator, clientIdValidator);
final NimbusJwtDecoder nimbusJwtDecoder = new NimbusJwtDecoder(jwtProcessor);
nimbusJwtDecoder.setJwtValidator(combinedValidator);
return nimbusJwtDecoder;
}
Comment From: Dieken
My service runs on Kubernetes, the jwks file is mounted into K8S pod with configmap volume, so it's a symlink in POD which content may change at any time. I get a workaround to support file URI for jwk-set-uri:
@Bean
public JwkSetUriJwtDecoderBuilderCustomizer fakeRestTemplatesForFileURI(
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwksUri) {
return builder -> {
if (jwksUri != null && jwksUri.startsWith("file:")) {
builder.restOperations(new RestTemplate() {
@Override
public <T> ResponseEntity<T> exchange(RequestEntity<?> entity, Class<T> responseType) throws RestClientException {
if (responseType != String.class) {
throw new IllegalArgumentException("responseType must be String.class");
}
try {
String content = Files.readString(Path.of(entity.getUrl().getSchemeSpecificPart()));
return (ResponseEntity<T>) ResponseEntity.ok(content);
} catch (IOException ex) {
throw new RestClientException("failed to read jwks file", ex);
}
}
});
}
};
}
This code depends on implementation details of class RestOperationsResourceRetriever, which calls only RestOperations.exchange(RequestEntity, Class), works for Spring Security 6.2.0.