diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index 4e5c357a18d..7a8bc38cdf1 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -58,6 +58,7 @@ import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; @@ -97,7 +98,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private final Converter> jwtProcessor; - private OAuth2TokenValidator jwtValidator = JwtValidators.createDefault(); + private Converter> jwtValidator; private Converter, Map> claimSetConverter = MappedJwtClaimSetConverter .withDefaults(Collections.emptyMap()); @@ -127,6 +128,7 @@ public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) { */ public NimbusReactiveJwtDecoder(Converter> jwtProcessor) { this.jwtProcessor = jwtProcessor; + setJwtValidator(JwtValidators.createDefault()); } /** @@ -134,6 +136,17 @@ public NimbusReactiveJwtDecoder(Converter> jwtProcessor) * @param jwtValidator the {@link OAuth2TokenValidator} to use */ public void setJwtValidator(OAuth2TokenValidator jwtValidator) { + Assert.notNull(jwtValidator, "jwtValidator cannot be null"); + this.jwtValidator = jwt -> Mono.fromSupplier(() -> jwtValidator.validate(jwt)) + .subscribeOn(Schedulers.boundedElastic()); + } + + /** + * Use the provided {@link Converter} to validate incoming {@link Jwt}s. This replaces + * the {@link OAuth2TokenValidator}, but allows for reactive validation. + * @param jwtValidator the {@link Converter} to use + */ + public void setJwtValidator(Converter> jwtValidator) { Assert.notNull(jwtValidator, "jwtValidator cannot be null"); this.jwtValidator = jwtValidator; } @@ -167,7 +180,7 @@ private Mono decode(JWT parsedToken) { // @formatter:off return this.jwtProcessor.convert(parsedToken) .map((set) -> createJwt(parsedToken, set)) - .map(this::validateJwt) + .flatMap(this::validateJwt) .onErrorMap((ex) -> !(ex instanceof IllegalStateException) && !(ex instanceof JwtException), (ex) -> new JwtException("An error occurred while attempting to decode the Jwt: ", ex)); // @formatter:on @@ -194,14 +207,17 @@ private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { } } - private Jwt validateJwt(Jwt jwt) { - OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt); - if (result.hasErrors()) { - Collection errors = result.getErrors(); - String validationErrorString = getJwtValidationExceptionMessage(errors); - throw new JwtValidationException(validationErrorString, errors); - } - return jwt; + private Mono validateJwt(Jwt jwt) { + return this.jwtValidator.convert(jwt).handle((result, sink) -> { + if (result.hasErrors()) { + Collection errors = result.getErrors(); + String validationErrorString = getJwtValidationExceptionMessage(errors); + sink.error(new JwtValidationException(validationErrorString, errors)); + } + else { + sink.next(jwt); + } + }); } private String getJwtValidationExceptionMessage(Collection errors) { diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java index 68584c28bec..c9fa4899ac3 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java @@ -293,7 +293,7 @@ public void decodeWhenClaimSetConverterFailsThenBadJwtException() { public void setJwtValidatorWhenGivenNullThrowsIllegalArgumentException() { // @formatter:off assertThatIllegalArgumentException() - .isThrownBy(() -> this.decoder.setJwtValidator(null)); + .isThrownBy(() -> this.decoder.setJwtValidator((OAuth2TokenValidator)null)); // @formatter:on } @@ -687,7 +687,7 @@ public void decodeWhenPublicKeyValidateTypeFalseThenSkipsNimbusTypeValidation() NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY) .validateType(false) .build(); - jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success()); + jwtDecoder.setJwtValidator((OAuth2TokenValidator) (jwt) -> OAuth2TokenValidatorResult.success()); RSAPrivateKey privateKey = TestKeys.DEFAULT_PRIVATE_KEY; SignedJWT jwt = signedJwt(privateKey, new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(), @@ -700,7 +700,7 @@ public void decodeWhenSecretKeyValidateTypeFalseThenSkipsNimbusTypeValidation() NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY) .validateType(false) .build(); - jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success()); + jwtDecoder.setJwtValidator((OAuth2TokenValidator) (jwt) -> OAuth2TokenValidatorResult.success()); SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY, new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JOSE).build(), new JWTClaimsSet.Builder().subject("subject").build()); @@ -715,7 +715,7 @@ public void decodeWhenJwkSourceValidateTypeFalseThenSkipsNimbusTypeValidation() NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSource((jwt) -> Flux.just(jwk)) .validateType(false) .build(); - jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success()); + jwtDecoder.setJwtValidator((OAuth2TokenValidator) (jwt) -> OAuth2TokenValidatorResult.success()); SignedJWT jwt = signedJwt(TestKeys.DEFAULT_PRIVATE_KEY, new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(), new JWTClaimsSet.Builder().subject("subject").build());