Skip to content

Commit c423ea6

Browse files
committed
Add builder to create NimbusJwtDecoder with JwkSource
Signed-off-by: Mark Bonnekessel <[email protected]>
1 parent 5517d8f commit c423ea6

File tree

2 files changed

+133
-1
lines changed

2 files changed

+133
-1
lines changed

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,16 @@ public static SecretKeyJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
261261
return new SecretKeyJwtDecoderBuilder(secretKey);
262262
}
263263

264+
/**
265+
* Use the given {@code JWKSource} to create a JwkSourceJwtDecoderBuilder.
266+
* @param jwkSource the JWK Source to use
267+
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
268+
* @since 7.0
269+
*/
270+
public static JwkSourceJwtDecoderBuilder withJwkSource(JWKSource<SecurityContext> jwkSource) {
271+
return new JwkSourceJwtDecoderBuilder(jwkSource);
272+
}
273+
264274
/**
265275
* A builder for creating {@link NimbusJwtDecoder} instances based on a
266276
* <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a>
@@ -274,7 +284,7 @@ public static final class JwkSetUriJwtDecoderBuilder {
274284
private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
275285
};
276286

277-
private final Function<RestOperations, String> jwkSetUri;
287+
private Function<RestOperations, String> jwkSetUri;
278288

279289
private Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms = (source) -> Set
280290
.of(JWSAlgorithm.RS256);
@@ -289,6 +299,8 @@ public static final class JwkSetUriJwtDecoderBuilder {
289299

290300
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
291301

302+
private JWKSource<SecurityContext> jwkSource;
303+
292304
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
293305
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
294306
this.jwkSetUri = (rest) -> jwkSetUri;
@@ -306,6 +318,13 @@ private JwkSetUriJwtDecoderBuilder(Function<RestOperations, String> jwkSetUri,
306318
};
307319
}
308320

321+
private JwkSetUriJwtDecoderBuilder(JWKSource<SecurityContext> jwkSource) {
322+
Assert.notNull(jwkSource, "jwkSource cannot be null");
323+
this.jwkSource = jwkSource;
324+
this.jwtProcessorCustomizer = (processor) -> {
325+
};
326+
}
327+
309328
/**
310329
* Whether to use Nimbus's typ header verification. This is {@code true} by
311330
* default, however it may change to {@code false} in a future major release.
@@ -436,6 +455,9 @@ JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSou
436455
}
437456

438457
JWKSource<SecurityContext> jwkSource() {
458+
if (this.jwkSource != null) {
459+
return this.jwkSource;
460+
}
439461
String jwkSetUri = this.jwkSetUri.apply(this.restOperations);
440462
return JWKSourceBuilder.create(new SpringJWKSource<>(this.restOperations, this.cache, jwkSetUri))
441463
.refreshAheadCache(false)
@@ -535,6 +557,106 @@ public void close() {
535557

536558
}
537559

560+
/**
561+
* A builder for creating {@link NimbusJwtDecoder} instances based on a {@code JWKSource}.
562+
*/
563+
public static final class JwkSourceJwtDecoderBuilder {
564+
565+
private static final JOSEObjectTypeVerifier<SecurityContext> NO_TYPE_VERIFIER = (header, context) -> {
566+
};
567+
568+
private final Function<JWKSource<SecurityContext>, Set<JWSAlgorithm>> defaultAlgorithms = (source) -> Set
569+
.of(JWSAlgorithm.RS256);
570+
571+
private final JOSEObjectTypeVerifier<SecurityContext> typeVerifier = NO_TYPE_VERIFIER;
572+
573+
private final Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
574+
575+
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
576+
577+
private final JWKSource<SecurityContext> jwkSource;
578+
579+
private JwkSourceJwtDecoderBuilder (JWKSource<SecurityContext> jwkSource) {
580+
Assert.notNull(jwkSource, "jwkSource cannot be null");
581+
this.jwkSource = jwkSource;
582+
this.jwtProcessorCustomizer = (processor) -> {
583+
};
584+
}
585+
586+
/**
587+
* Append the given signing
588+
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
589+
* "_blank">algorithm</a> to the set of algorithms to use.
590+
* @param signatureAlgorithm the algorithm to use
591+
* @return a {@link JwkSourceJwtDecoderBuilder } for further configurations
592+
*/
593+
public JwkSourceJwtDecoderBuilder jwsAlgorithm(SignatureAlgorithm signatureAlgorithm) {
594+
Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
595+
this.signatureAlgorithms.add(signatureAlgorithm);
596+
return this;
597+
}
598+
599+
/**
600+
* Configure the list of
601+
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target=
602+
* "_blank">algorithms</a> to use with the given {@link Consumer}.
603+
* @param signatureAlgorithmsConsumer a {@link Consumer} for further configuring
604+
* the algorithm list
605+
* @return a {@link JwkSourceJwtDecoderBuilder } for further configurations
606+
*/
607+
public JwkSourceJwtDecoderBuilder jwsAlgorithms(Consumer<Set<SignatureAlgorithm>> signatureAlgorithmsConsumer) {
608+
Assert.notNull(signatureAlgorithmsConsumer, "signatureAlgorithmsConsumer cannot be null");
609+
signatureAlgorithmsConsumer.accept(this.signatureAlgorithms);
610+
return this;
611+
}
612+
613+
/**
614+
* Use the given {@link Consumer} to customize the {@link JWTProcessor
615+
* ConfigurableJWTProcessor} before passing it to the build
616+
* {@link NimbusJwtDecoder}.
617+
* @param jwtProcessorCustomizer the callback used to alter the processor
618+
* @return a {@link JwkSourceJwtDecoderBuilder } for further configurations
619+
* @since 5.4
620+
*/
621+
public JwkSourceJwtDecoderBuilder jwtProcessorCustomizer(
622+
Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer) {
623+
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
624+
this.jwtProcessorCustomizer = jwtProcessorCustomizer;
625+
return this;
626+
}
627+
628+
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
629+
if (this.signatureAlgorithms.isEmpty()) {
630+
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
631+
}
632+
Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
633+
for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
634+
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
635+
jwsAlgorithms.add(jwsAlgorithm);
636+
}
637+
return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
638+
}
639+
640+
JWTProcessor<SecurityContext> processor() {
641+
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
642+
jwtProcessor.setJWSTypeVerifier(this.typeVerifier);
643+
jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource));
644+
// Spring Security validates the claim set independent from Nimbus
645+
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
646+
});
647+
this.jwtProcessorCustomizer.accept(jwtProcessor);
648+
return jwtProcessor;
649+
}
650+
651+
/**
652+
* Build the configured {@link NimbusJwtDecoder}.
653+
* @return the configured {@link NimbusJwtDecoder}
654+
*/
655+
public NimbusJwtDecoder build() {
656+
return new NimbusJwtDecoder(processor());
657+
}
658+
}
659+
538660
/**
539661
* A builder for creating {@link NimbusJwtDecoder} instances based on a public key.
540662
*/

oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.nimbusds.jose.JWSSigner;
4343
import com.nimbusds.jose.crypto.MACSigner;
4444
import com.nimbusds.jose.crypto.RSASSASigner;
45+
import com.nimbusds.jose.jwk.JWKSet;
4546
import com.nimbusds.jose.jwk.source.JWKSource;
4647
import com.nimbusds.jose.proc.BadJOSEException;
4748
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
@@ -557,6 +558,15 @@ public void decodeWhenUsingSecretKeyWithKidThenStillUsesKey() throws Exception {
557558
// @formatter:on
558559
}
559560

561+
@Test
562+
public void withJwkSourceWhenDefaultsThenUsesProvidedJwkSource() throws Exception {
563+
JWKSource<SecurityContext> source = mock(JWKSource.class);
564+
given(source.get(any(), any())).willReturn(JWKSet.parse(JWK_SET).getKeys());
565+
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSource(source).build();
566+
Jwt jwt = decoder.decode(SIGNED_JWT);
567+
assertThat(jwt.getClaimAsString("sub")).isEqualTo("test-subject");
568+
}
569+
560570
// gh-8730
561571
@Test
562572
public void withSecretKeyWhenUsingCustomTypeHeaderThenSuccessfullyDecodes() throws Exception {

0 commit comments

Comments
 (0)