diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index 3231906ce0..0c88ca1b4f 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java @@ -87,6 +87,7 @@ * @author Mykyta Bezverkhyi * @author Daeho Kwon * @author Andrey Litvitski + * @author Ngoc Nhan * @since 5.2 */ public final class NimbusJwtDecoder implements JwtDecoder { @@ -306,6 +307,8 @@ public static final class JwkSetUriJwtDecoderBuilder { private Consumer> jwtProcessorCustomizer; + private @Nullable Consumer> jwkSourceBuilderCustomizer; + private OAuth2TokenValidator validator = JwtValidators.createDefault(); private JwkSetUriJwtDecoderBuilder(String jwkSetUri) { @@ -448,6 +451,31 @@ public JwkSetUriJwtDecoderBuilder jwtProcessorCustomizer( return this; } + /** + * Use the given {@link Consumer} to customize the {@link JWKSourceBuilder} before + * passing it to the {@link NimbusJwtDecoder} build process. + * + *

+ * By default, the {@link JWKSourceBuilder} is configured as follows: + * + *

+		 * jwkSourceBuilder
+		 *     .refreshAheadCache(false)
+		 *     .rateLimited(false)
+		 *     .cache(this.cache instanceof NoOpCache);
+		 * 
+ * @param jwkSourceBuilderCustomizer the callback used to customize the + * {@link JWKSourceBuilder} + * @return a {@link JwkSetUriJwtDecoderBuilder} for further configuration + * @since 7.1 + */ + public JwkSetUriJwtDecoderBuilder jwkSourceBuilderCustomizer( + Consumer> jwkSourceBuilderCustomizer) { + Assert.notNull(jwkSourceBuilderCustomizer, "jwkSourceBuilderCustomizer cannot be null"); + this.jwkSourceBuilderCustomizer = jwkSourceBuilderCustomizer; + return this; + } + JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator validator) { Assert.notNull(validator, "validator cannot be null"); this.validator = validator; @@ -468,11 +496,15 @@ JWSKeySelector jwsKeySelector(JWKSource jwkSou JWKSource jwkSource() { String jwkSetUri = this.jwkSetUri.apply(this.restOperations); - return JWKSourceBuilder.create(new SpringJWKSource<>(this.restOperations, this.cache, jwkSetUri)) + JWKSourceBuilder jwkSourceBuilder = JWKSourceBuilder + .create(new SpringJWKSource<>(this.restOperations, this.cache, jwkSetUri)) .refreshAheadCache(false) .rateLimited(false) - .cache(this.cache instanceof NoOpCache) - .build(); + .cache(this.cache instanceof NoOpCache); + if (this.jwkSourceBuilderCustomizer != null) { + this.jwkSourceBuilderCustomizer.accept(jwkSourceBuilder); + } + return jwkSourceBuilder.build(); } JWTProcessor processor() { diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java index 17cbb2b309..51a0928c8c 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java @@ -43,7 +43,10 @@ import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.CachingJWKSetSource; import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.RefreshAheadCachingJWKSetSource; +import com.nimbusds.jose.jwk.source.RetryingJWKSetSource; import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; import com.nimbusds.jose.proc.JWSKeySelector; @@ -99,6 +102,7 @@ * @author Joe Grandja * @author Mykyta Bezverkhyi * @author Andrey Litvitski + * @author Ngoc Nhan */ public class NimbusJwtDecoderTests { @@ -929,6 +933,46 @@ public void decodeWhenSecretKeyValidateTypeFalseThenSkipsNimbusTypeValidation() jwtDecoder.decode(jwt.serialize()); } + @Test + public void shouldUseCachingJwkSetSource() { + + DefaultJWTProcessor jwtProcessor = (DefaultJWTProcessor) NimbusJwtDecoder + .withJwkSetUri(JWK_SET_URI) + .processor(); + Object jwkSource = ReflectionTestUtils.getField(jwtProcessor.getJWSKeySelector(), + JWSVerificationKeySelector.class, "jwkSource"); + assertThat(jwkSource).isNotNull(); + assertThat(ReflectionTestUtils.getField(jwkSource, "source")).isInstanceOf(CachingJWKSetSource.class); + } + + @Test + public void shouldUseRefreshAheadCachingJWKSetSource() { + + DefaultJWTProcessor jwtProcessor = (DefaultJWTProcessor) NimbusJwtDecoder + .withJwkSetUri(JWK_SET_URI) + .jwkSourceBuilderCustomizer( + (jwkSourceBuilder) -> jwkSourceBuilder.refreshAheadCache(true).rateLimited(true)) + .processor(); + Object jwkSource = ReflectionTestUtils.getField(jwtProcessor.getJWSKeySelector(), + JWSVerificationKeySelector.class, "jwkSource"); + assertThat(jwkSource).isNotNull(); + assertThat(ReflectionTestUtils.getField(jwkSource, "source")) + .isInstanceOf(RefreshAheadCachingJWKSetSource.class); + } + + @Test + public void shouldRetryingJWKSetSource() { + + DefaultJWTProcessor jwtProcessor = (DefaultJWTProcessor) NimbusJwtDecoder + .withJwkSetUri(JWK_SET_URI) + .jwkSourceBuilderCustomizer((jwkSourceBuilder) -> jwkSourceBuilder.cache(false).retrying(true)) + .processor(); + Object jwkSource = ReflectionTestUtils.getField(jwtProcessor.getJWSKeySelector(), + JWSVerificationKeySelector.class, "jwkSource"); + assertThat(jwkSource).isNotNull(); + assertThat(ReflectionTestUtils.getField(jwkSource, "source")).isInstanceOf(RetryingJWKSetSource.class); + } + private RSAPublicKey key() throws InvalidKeySpecException { byte[] decoded = Base64.getDecoder().decode(VERIFY_KEY.getBytes()); EncodedKeySpec spec = new X509EncodedKeySpec(decoded);