From ada75e76a6a9c435a842916bf1e8061c43215b67 Mon Sep 17 00:00:00 2001 From: Mark Bonnekessel <2949525+marbon87@users.noreply.github.com> Date: Fri, 16 May 2025 15:49:00 +0200 Subject: [PATCH] Add builder to create NimbusJwtDecoder with JwkSource Signed-off-by: Mark Bonnekessel <2949525+marbon87@users.noreply.github.com> --- .../security/oauth2/jwt/NimbusJwtDecoder.java | 112 ++++++++++++++++++ .../oauth2/jwt/NimbusJwtDecoderTests.java | 10 ++ 2 files changed, 122 insertions(+) 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 66f0b70e45..d762e919ac 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 @@ -261,6 +261,16 @@ public final class NimbusJwtDecoder implements JwtDecoder { return new SecretKeyJwtDecoderBuilder(secretKey); } + /** + * Use the given {@code JWKSource} to create a JwkSourceJwtDecoderBuilder. + * @param jwkSource the JWK Source to use + * @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations + * @since 7.0 + */ + public static JwkSourceJwtDecoderBuilder withJwkSource(JWKSource jwkSource) { + return new JwkSourceJwtDecoderBuilder(jwkSource); + } + /** * A builder for creating {@link NimbusJwtDecoder} instances based on a * JWK Set @@ -535,6 +545,108 @@ public final class NimbusJwtDecoder implements JwtDecoder { } + /** + * A builder for creating {@link NimbusJwtDecoder} instances based on a + * {@code JWKSource}. + */ + public static final class JwkSourceJwtDecoderBuilder { + + private static final JOSEObjectTypeVerifier NO_TYPE_VERIFIER = (header, context) -> { + }; + + private final Function, Set> defaultAlgorithms = (source) -> Set + .of(JWSAlgorithm.RS256); + + private final JOSEObjectTypeVerifier typeVerifier = NO_TYPE_VERIFIER; + + private final Set signatureAlgorithms = new HashSet<>(); + + private Consumer> jwtProcessorCustomizer; + + private final JWKSource jwkSource; + + private JwkSourceJwtDecoderBuilder(JWKSource jwkSource) { + Assert.notNull(jwkSource, "jwkSource cannot be null"); + this.jwkSource = jwkSource; + this.jwtProcessorCustomizer = (processor) -> { + }; + } + + /** + * Append the given signing + * algorithm to the set of algorithms to use. + * @param signatureAlgorithm the algorithm to use + * @return a {@link JwkSourceJwtDecoderBuilder } for further configurations + */ + public JwkSourceJwtDecoderBuilder jwsAlgorithm(SignatureAlgorithm signatureAlgorithm) { + Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null"); + this.signatureAlgorithms.add(signatureAlgorithm); + return this; + } + + /** + * Configure the list of + * algorithms to use with the given {@link Consumer}. + * @param signatureAlgorithmsConsumer a {@link Consumer} for further configuring + * the algorithm list + * @return a {@link JwkSourceJwtDecoderBuilder } for further configurations + */ + public JwkSourceJwtDecoderBuilder jwsAlgorithms(Consumer> signatureAlgorithmsConsumer) { + Assert.notNull(signatureAlgorithmsConsumer, "signatureAlgorithmsConsumer cannot be null"); + signatureAlgorithmsConsumer.accept(this.signatureAlgorithms); + return this; + } + + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor + * ConfigurableJWTProcessor} before passing it to the build + * {@link NimbusJwtDecoder}. + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link JwkSourceJwtDecoderBuilder } for further configurations + * @since 5.4 + */ + public JwkSourceJwtDecoderBuilder jwtProcessorCustomizer( + Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + + JWSKeySelector jwsKeySelector(JWKSource jwkSource) { + if (this.signatureAlgorithms.isEmpty()) { + return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource); + } + Set jwsAlgorithms = new HashSet<>(); + for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); + jwsAlgorithms.add(jwsAlgorithm); + } + return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource); + } + + JWTProcessor processor() { + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSTypeVerifier(this.typeVerifier); + jwtProcessor.setJWSKeySelector(jwsKeySelector(this.jwkSource)); + // Spring Security validates the claim set independent from Nimbus + jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { + }); + this.jwtProcessorCustomizer.accept(jwtProcessor); + return jwtProcessor; + } + + /** + * Build the configured {@link NimbusJwtDecoder}. + * @return the configured {@link NimbusJwtDecoder} + */ + public NimbusJwtDecoder build() { + return new NimbusJwtDecoder(processor()); + } + + } + /** * A builder for creating {@link NimbusJwtDecoder} instances based on a public key. */ 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 91db639c45..7dd353d8f1 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 @@ -42,6 +42,7 @@ import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.BadJOSEException; import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; @@ -557,6 +558,15 @@ public class NimbusJwtDecoderTests { // @formatter:on } + @Test + public void withJwkSourceWhenDefaultsThenUsesProvidedJwkSource() throws Exception { + JWKSource source = mock(JWKSource.class); + given(source.get(any(), any())).willReturn(JWKSet.parse(JWK_SET).getKeys()); + NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSource(source).build(); + Jwt jwt = decoder.decode(SIGNED_JWT); + assertThat(jwt.getClaimAsString("sub")).isEqualTo("test-subject"); + } + // gh-8730 @Test public void withSecretKeyWhenUsingCustomTypeHeaderThenSuccessfullyDecodes() throws Exception {