From 372ef86bdd52bfbb78a516739c22aa69dad7406c Mon Sep 17 00:00:00 2001 From: Iain Henderson Date: Sun, 11 Jan 2026 09:48:00 -0500 Subject: [PATCH 1/2] Make the reactive oauth stack more reactive by allowing OAuth2TokenValidators to be reactive. Signed-off-by: Iain Henderson --- ...eactiveDelegatingOAuth2TokenValidator.java | 95 ++++++++++++++ .../core/ReactiveOAuth2TokenValidator.java | 37 ++++++ .../ReactiveWrappingOAuth2TokenValidator.java | 45 +++++++ ...veDelegatingOAuth2TokenValidatorTests.java | 118 ++++++++++++++++++ ...tiveWrappingOAuth2TokenValidatorTests.java | 57 +++++++++ .../security/oauth2/jwt/JwtValidators.java | 27 ++-- .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 36 ++++-- .../jwt/NimbusReactiveJwtDecoderTests.java | 10 +- 8 files changed, 401 insertions(+), 24 deletions(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveOAuth2TokenValidator.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidatorTests.java create mode 100644 oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidatorTests.java diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java new file mode 100644 index 0000000000..9e62d23f7d --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java @@ -0,0 +1,95 @@ +/* + * Copyright 2025-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core; + +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import static java.util.Collections.emptyList; + +/** + * A reactive composite validator + * + * @param the type of {@link OAuth2Token} this validator validates + * @author Josh Cummings + * @author Iain Henderson + */ +public final class ReactiveDelegatingOAuth2TokenValidator implements ReactiveOAuth2TokenValidator { + + private final Collection> tokenValidators; + private final Collection> reactiveTokenValidators; + + /** + * Constructs a {@code ReactiveDelegatingOAuth2TokenValidator} using the provided validators. + * @param tokenValidators the {@link Collection} of {@link OAuth2TokenValidator}s to + * use + * @param reactiveTokenValidators the {@link Collection} of {@link ReactiveOAuth2TokenValidator}s to + * use + */ + public ReactiveDelegatingOAuth2TokenValidator(Collection> tokenValidators, + Collection> reactiveTokenValidators) { + Assert.notNull(tokenValidators, "tokenValidators cannot be null"); + Assert.notNull(reactiveTokenValidators, "reactiveTokenValidators cannot be null"); + this.tokenValidators = new ArrayList<>(tokenValidators); + this.reactiveTokenValidators = new ArrayList<>(reactiveTokenValidators); + } + + /** + * Constructs a {@code ReactiveDelegatingOAuth2TokenValidator} using the provided validators. + * @param reactiveTokenValidators the {@link Collection} of {@link ReactiveOAuth2TokenValidator}s to + * use + */ + public ReactiveDelegatingOAuth2TokenValidator(Collection> reactiveTokenValidators) { + this(emptyList(), reactiveTokenValidators); + } + + /** + * Constructs a {@code ReactiveDelegatingOAuth2TokenValidator} using the provided validators. + * @param tokenValidators the collection of {@link OAuth2TokenValidator}s to use + */ + @SafeVarargs + public ReactiveDelegatingOAuth2TokenValidator(OAuth2TokenValidator... tokenValidators) { + this(Arrays.asList(tokenValidators), emptyList()); + } + + /** + * Constructs a {@code ReactiveDelegatingOAuth2TokenValidator} using the provided validators. + * @param reactiveTokenValidators the collection of {@link ReactiveOAuth2TokenValidator}s to use + */ + @SafeVarargs + public ReactiveDelegatingOAuth2TokenValidator(ReactiveOAuth2TokenValidator... reactiveTokenValidators) { + this(Arrays.asList(reactiveTokenValidators)); + } + + @Override + public Mono validate(T token) { + return Flux.fromIterable(this.tokenValidators) + .map(validator -> validator.validate(token)) + .mergeWith(Flux.fromIterable(reactiveTokenValidators) + .flatMap(validator -> validator.validate(token))) + .map(OAuth2TokenValidatorResult::getErrors) + .flatMap(Flux::fromIterable) + .collectList() + .map(OAuth2TokenValidatorResult::failure); + } + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveOAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveOAuth2TokenValidator.java new file mode 100644 index 0000000000..8e2e234588 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveOAuth2TokenValidator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core; + +import reactor.core.publisher.Mono; + +/** + * Implementations of this interface are responsible for "verifying" the + * validity and/or constraints of the attributes contained in an OAuth 2.0 Token. + * + * @author Iain Henderson + */ +@FunctionalInterface +public interface ReactiveOAuth2TokenValidator { + + /** + * Verify the validity and/or constraints of the provided OAuth 2.0 Token. + * @param token an OAuth 2.0 token + * @return Mono the success or failure detail of the validation + */ + Mono validate(T token); + +} diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java new file mode 100644 index 0000000000..56211e3c49 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core; + +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +/** + * A reactive wrapper for synchronous validators + * + * @param the type of {@link OAuth2Token} this validator validates + * @author Iain Henderson + */ +public final class ReactiveWrappingOAuth2TokenValidator implements ReactiveOAuth2TokenValidator { + + private final OAuth2TokenValidator tokenValidator; + + /** + * Constructs a {@code ReactiveWrappingOAuth2TokenValidator} using the provided validator. + * @param tokenValidator the {@link OAuth2TokenValidator}s to use + */ + public ReactiveWrappingOAuth2TokenValidator(OAuth2TokenValidator tokenValidator) { + Assert.notNull(tokenValidator, "tokenValidator cannot be null"); + this.tokenValidator = tokenValidator; + } + + @Override + public Mono validate(T token) { + return Mono.just(tokenValidator.validate(token)); + } +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidatorTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidatorTests.java new file mode 100644 index 0000000000..c3d47ddbc7 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidatorTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.Collection; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +/** + * Tests for verifying {@link ReactiveDelegatingOAuth2TokenValidator} + * + * @author Josh Cummings + * @author Iain Henderson + */ +public class ReactiveDelegatingOAuth2TokenValidatorTests { + + private static final OAuth2Error DETAIL = new OAuth2Error("error", "description", "uri"); + + @Test + public void validateWhenNoValidatorsConfiguredThenReturnsSuccessfulResult() { + ReactiveDelegatingOAuth2TokenValidator tokenValidator = + new ReactiveDelegatingOAuth2TokenValidator<>(emptyList()); + OAuth2Token token = mock(OAuth2Token.class); + assertThat(tokenValidator.validate(token).block().hasErrors()).isFalse(); + } + + @Test + public void validateWhenAnyValidatorFailsThenReturnsFailureResultContainingDetailFromFailingValidator() { + OAuth2TokenValidator success = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator failure = mock(OAuth2TokenValidator.class); + given(success.validate(any(OAuth2Token.class))).willReturn(OAuth2TokenValidatorResult.success()); + given(failure.validate(any(OAuth2Token.class))).willReturn(OAuth2TokenValidatorResult.failure(DETAIL)); + ReactiveDelegatingOAuth2TokenValidator tokenValidator = new ReactiveDelegatingOAuth2TokenValidator<>( + success, failure); + OAuth2Token token = mock(OAuth2Token.class); + OAuth2TokenValidatorResult result = tokenValidator.validate(token).block(); + assertThat(result).isNotNull(); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors()).containsExactly(DETAIL); + } + + @Test + public void validateWhenMultipleValidatorsFailThenReturnsFailureResultContainingAllDetails() { + OAuth2TokenValidator firstFailure = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator secondFailure = mock(OAuth2TokenValidator.class); + OAuth2Error otherDetail = new OAuth2Error("another-error"); + given(firstFailure.validate(any(OAuth2Token.class))).willReturn(OAuth2TokenValidatorResult.failure(DETAIL)); + given(secondFailure.validate(any(OAuth2Token.class))) + .willReturn(OAuth2TokenValidatorResult.failure(otherDetail)); + ReactiveDelegatingOAuth2TokenValidator tokenValidator = new ReactiveDelegatingOAuth2TokenValidator<>(firstFailure, + secondFailure); + OAuth2Token token = mock(OAuth2Token.class); + OAuth2TokenValidatorResult result = tokenValidator.validate(token).block(); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors()).containsExactly(DETAIL, otherDetail); + } + + @Test + public void validateWhenAllValidatorsSucceedThenReturnsSuccessfulResult() { + OAuth2TokenValidator firstSuccess = mock(OAuth2TokenValidator.class); + OAuth2TokenValidator secondSuccess = mock(OAuth2TokenValidator.class); + given(firstSuccess.validate(any(OAuth2Token.class))).willReturn(OAuth2TokenValidatorResult.success()); + given(secondSuccess.validate(any(OAuth2Token.class))).willReturn(OAuth2TokenValidatorResult.success()); + ReactiveDelegatingOAuth2TokenValidator tokenValidator = + new ReactiveDelegatingOAuth2TokenValidator<>(firstSuccess, secondSuccess); + OAuth2Token token = mock(OAuth2Token.class); + OAuth2TokenValidatorResult result = tokenValidator.validate(token).block(); + assertThat(result.hasErrors()).isFalse(); + assertThat(result.getErrors()).isEmpty(); + } + + @Test + public void constructorWhenInvokedWithNullValidatorListThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new ReactiveDelegatingOAuth2TokenValidator<>((Collection>) null)); + } + + @Test + public void constructorsWhenInvokedWithSameInputsThenResultInSameOutputs() { + ReactiveOAuth2TokenValidator firstSuccess = mock(ReactiveOAuth2TokenValidator.class); + ReactiveOAuth2TokenValidator secondSuccess = mock(ReactiveOAuth2TokenValidator.class); + given(firstSuccess.validate(any(OAuth2Token.class))).willReturn(Mono.just(OAuth2TokenValidatorResult.success())); + given(secondSuccess.validate(any(OAuth2Token.class))).willReturn(Mono.just(OAuth2TokenValidatorResult.success())); + ReactiveDelegatingOAuth2TokenValidator firstValidator = + new ReactiveDelegatingOAuth2TokenValidator<>(Arrays.asList(firstSuccess, secondSuccess)); + ReactiveDelegatingOAuth2TokenValidator secondValidator = + new ReactiveDelegatingOAuth2TokenValidator<>(firstSuccess, secondSuccess); + OAuth2Token token = mock(OAuth2Token.class); + firstValidator.validate(token).block(); + secondValidator.validate(token).block(); + verify(firstSuccess, times(2)).validate(token); + verify(secondSuccess, times(2)).validate(token); + } + +} diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidatorTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidatorTests.java new file mode 100644 index 0000000000..d7cba2d238 --- /dev/null +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidatorTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.core; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.*; + +/** + * Tests for verifying {@link ReactiveWrappingOAuth2TokenValidatorTests} + * + * @author Iain Henderson + */ +public class ReactiveWrappingOAuth2TokenValidatorTests { + + private static final OAuth2Error DETAIL = new OAuth2Error("error", "description", "uri"); + + @Test + public void validate() { + ReactiveWrappingOAuth2TokenValidator tokenValidator = + new ReactiveWrappingOAuth2TokenValidator<>(token -> OAuth2TokenValidatorResult.success()); + OAuth2Token token = mock(OAuth2Token.class); + assertThat(tokenValidator.validate(token).block().hasErrors()).isFalse(); + } + + @Test + public void validateFailure() { + ReactiveWrappingOAuth2TokenValidator tokenValidator = + new ReactiveWrappingOAuth2TokenValidator<>(token -> OAuth2TokenValidatorResult.failure(DETAIL)); + OAuth2Token token = mock(OAuth2Token.class); + OAuth2TokenValidatorResult result = tokenValidator.validate(token).block(); + assertThat(result).isNotNull(); + assertThat(result.hasErrors()).isTrue(); + assertThat(result.getErrors()).containsExactly(DETAIL); + } + + @Test + public void constructorWhenInvokedWithNullValidatorListThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new ReactiveWrappingOAuth2TokenValidator<>(null)); + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java index 7566f3b607..ceab0e4d54 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java @@ -24,11 +24,7 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Predicate; -import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2ErrorCodes; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.core.*; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -37,6 +33,7 @@ import org.springframework.util.CollectionUtils; * * @author Josh Cummings * @author Rob Winch + * @author Iain Henderson * @since 5.1 */ public final class JwtValidators { @@ -71,8 +68,7 @@ public final class JwtValidators { * result of this method to {@code DelegatingOAuth2TokenValidator} along with the * additional validators. *

- * @return - a delegating validator containing all standard validators as well as any - * supplied + * @return - a delegating validator containing all standard validators */ public static OAuth2TokenValidator createDefault() { return new DelegatingOAuth2TokenValidator<>(Arrays.asList(JwtTypeValidator.jwt(), new JwtTimestampValidator(), @@ -80,6 +76,23 @@ public final class JwtValidators { X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER))); } + /** + *

+ * Create a reactive {@link Jwt} Validator that contains all standard validators. + *

+ *

+ * User's wanting to leverage the defaults plus additional validation can add the + * result of this method to {@code ReactiveDelegatingOAuth2TokenValidator} along with the + * additional validators. + *

+ * @return - a reactive delegating validator containing all standard validators + */ + public static ReactiveOAuth2TokenValidator createReactiveDefault() { + return new ReactiveDelegatingOAuth2TokenValidator<>(JwtTypeValidator.jwt(), new JwtTimestampValidator(), + new X509CertificateThumbprintValidator( + X509CertificateThumbprintValidator.DEFAULT_X509_CERTIFICATE_SUPPLIER)); + } + /** *

* Create a {@link Jwt} default validator with standard validators and additional 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 2b8e58c549..597cf51907 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 @@ -55,15 +55,13 @@ import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.nimbusds.jwt.proc.JWTProcessor; +import org.springframework.security.oauth2.core.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import org.springframework.core.convert.converter.Converter; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.OAuth2TokenValidator; -import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -96,7 +94,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private final Converter> jwtProcessor; - private OAuth2TokenValidator jwtValidator = JwtValidators.createDefault(); + private ReactiveOAuth2TokenValidator jwtValidator = JwtValidators.createReactiveDefault(); private Converter, Map> claimSetConverter = MappedJwtClaimSetConverter .withDefaults(Collections.emptyMap()); @@ -133,6 +131,15 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { * @param jwtValidator the {@link OAuth2TokenValidator} to use */ public void setJwtValidator(OAuth2TokenValidator jwtValidator) { + Assert.notNull(jwtValidator, "jwtValidator cannot be null"); + this.jwtValidator = new ReactiveWrappingOAuth2TokenValidator<>(jwtValidator); + } + + /** + * Use the provided {@link OAuth2TokenValidator} to validate incoming {@link Jwt}s. + * @param jwtValidator the {@link OAuth2TokenValidator} to use + */ + public void setJwtValidator(ReactiveOAuth2TokenValidator jwtValidator) { Assert.notNull(jwtValidator, "jwtValidator cannot be null"); this.jwtValidator = jwtValidator; } @@ -166,7 +173,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { // @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 @@ -193,14 +200,17 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { } } - 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.validate(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 beb5966b9b..ca5acff2ef 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 @@ -58,6 +58,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.core.ReactiveOAuth2TokenValidator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -84,6 +85,7 @@ import static org.mockito.Mockito.verify; /** * @author Rob Winch * @author Joe Grandja + * @author Iain Henderson * @since 5.1 */ public class NimbusReactiveJwtDecoderTests { @@ -293,7 +295,7 @@ public class NimbusReactiveJwtDecoderTests { public void setJwtValidatorWhenGivenNullThrowsIllegalArgumentException() { // @formatter:off assertThatIllegalArgumentException() - .isThrownBy(() -> this.decoder.setJwtValidator(null)); + .isThrownBy(() -> this.decoder.setJwtValidator((ReactiveOAuth2TokenValidator) null)); // @formatter:on } @@ -667,7 +669,7 @@ public class NimbusReactiveJwtDecoderTests { 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(), @@ -680,7 +682,7 @@ public class NimbusReactiveJwtDecoderTests { 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()); @@ -695,7 +697,7 @@ public class NimbusReactiveJwtDecoderTests { 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()); From a6cf37a4f2b3285ad6313bf1284355fcf3e25bf6 Mon Sep 17 00:00:00 2001 From: Iain Henderson Date: Sat, 14 Mar 2026 08:49:59 -0400 Subject: [PATCH 2/2] Cleanup formatting --- ...eactiveDelegatingOAuth2TokenValidator.java | 14 +- .../ReactiveWrappingOAuth2TokenValidator.java | 3 +- .../security/oauth2/jwt/JwtValidators.java | 8 +- .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 229 +++++++++--------- .../jwt/NimbusReactiveJwtDecoderTests.java | 123 ++++------ 5 files changed, 183 insertions(+), 194 deletions(-) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java index 9e62d23f7d..8496c51280 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveDelegatingOAuth2TokenValidator.java @@ -16,15 +16,15 @@ package org.springframework.security.oauth2.core; -import org.springframework.util.Assert; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; -import static java.util.Collections.emptyList; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; /** * A reactive composite validator @@ -59,7 +59,7 @@ public final class ReactiveDelegatingOAuth2TokenValidator * use */ public ReactiveDelegatingOAuth2TokenValidator(Collection> reactiveTokenValidators) { - this(emptyList(), reactiveTokenValidators); + this(Collections.emptyList(), reactiveTokenValidators); } /** @@ -68,7 +68,7 @@ public final class ReactiveDelegatingOAuth2TokenValidator */ @SafeVarargs public ReactiveDelegatingOAuth2TokenValidator(OAuth2TokenValidator... tokenValidators) { - this(Arrays.asList(tokenValidators), emptyList()); + this(Arrays.asList(tokenValidators), Collections.emptyList()); } /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java index 56211e3c49..ca5f2ef9e5 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java @@ -16,9 +16,10 @@ package org.springframework.security.oauth2.core; -import org.springframework.util.Assert; import reactor.core.publisher.Mono; +import org.springframework.util.Assert; + /** * A reactive wrapper for synchronous validators * diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java index ceab0e4d54..bca7d30055 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtValidators.java @@ -24,7 +24,13 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Predicate; -import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.core.ReactiveDelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.ReactiveOAuth2TokenValidator; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; 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 597cf51907..ab982ec375 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 @@ -55,13 +55,16 @@ import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.nimbusds.jwt.proc.JWTProcessor; -import org.springframework.security.oauth2.core.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.core.ReactiveOAuth2TokenValidator; +import org.springframework.security.oauth2.core.ReactiveWrappingOAuth2TokenValidator; import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -97,7 +100,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private ReactiveOAuth2TokenValidator jwtValidator = JwtValidators.createReactiveDefault(); private Converter, Map> claimSetConverter = MappedJwtClaimSetConverter - .withDefaults(Collections.emptyMap()); + .withDefaults(Collections.emptyMap()); /** * Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters. @@ -126,6 +129,96 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { this.jwtProcessor = jwtProcessor; } + /** + * Use the given Issuer + * by making an OpenID + * Provider Configuration Request and using the values in the OpenID + * Provider Configuration Response to derive the needed + * JWK Set uri. + * @param issuer the Issuer + * @return a {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder} that will derive the + * JWK Set uri when {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder#build} is + * called + * @since 6.1 + * @see JwtDecoders + */ + public static JwkSetUriReactiveJwtDecoderBuilder withIssuerLocation(String issuer) { + return new JwkSetUriReactiveJwtDecoderBuilder( + (web) -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(issuer, web) + .flatMap((configuration) -> { + try { + JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); + } + catch (IllegalStateException ex) { + return Mono.error(ex); + } + return Mono.just(configuration.get("jwks_uri").toString()); + }), + ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms); + } + + /** + * Use the given JWK Set + * uri to validate JWTs. + * @param jwkSetUri the JWK Set uri to use + * @return a {@link JwkSetUriReactiveJwtDecoderBuilder} for further configurations + * + * @since 5.2 + */ + public static JwkSetUriReactiveJwtDecoderBuilder withJwkSetUri(String jwkSetUri) { + return new JwkSetUriReactiveJwtDecoderBuilder(jwkSetUri); + } + + /** + * Use the given public key to validate JWTs + * @param key the public key to use + * @return a {@link PublicKeyReactiveJwtDecoderBuilder} for further configurations + * + * @since 5.2 + */ + public static PublicKeyReactiveJwtDecoderBuilder withPublicKey(RSAPublicKey key) { + return new PublicKeyReactiveJwtDecoderBuilder(key); + } + + /** + * Use the given {@code SecretKey} to validate the MAC on a JSON Web Signature (JWS). + * @param secretKey the {@code SecretKey} used to validate the MAC + * @return a {@link SecretKeyReactiveJwtDecoderBuilder} for further configurations + * + * @since 5.2 + */ + public static SecretKeyReactiveJwtDecoderBuilder withSecretKey(SecretKey secretKey) { + return new SecretKeyReactiveJwtDecoderBuilder(secretKey); + } + + /** + * Use the given {@link Function} to validate JWTs + * @param source the {@link Function} + * @return a {@link JwkSourceReactiveJwtDecoderBuilder} for further configurations + * + * @since 5.2 + */ + public static JwkSourceReactiveJwtDecoderBuilder withJwkSource(Function> source) { + return new JwkSourceReactiveJwtDecoderBuilder(source); + } + + private static JWTClaimsSet createClaimsSet(JWTProcessor jwtProcessor, + JWT parsedToken, C context) { + try { + return jwtProcessor.process(parsedToken, context); + } + catch (BadJOSEException ex) { + throw new BadJwtException("Failed to validate the token", ex); + } + catch (JOSEException ex) { + throw new JwtException("Failed to validate the token", ex); + } + } + /** * Use the provided {@link OAuth2TokenValidator} to validate incoming {@link Jwt}s. * @param jwtValidator the {@link OAuth2TokenValidator} to use @@ -191,9 +284,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { Map headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject()); Map claims = this.claimSetConverter.convert(jwtClaimsSet.getClaims()); return Jwt.withTokenValue(parsedJwt.getParsedString()) - .headers((h) -> h.putAll(headers)) - .claims((c) -> c.putAll(claims)) - .build(); + .headers((h) -> h.putAll(headers)) + .claims((c) -> c.putAll(claims)) + .build(); } catch (Exception ex) { throw new BadJwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); @@ -222,96 +315,6 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { return "Unable to validate Jwt"; } - /** - * Use the given Issuer - * by making an OpenID - * Provider Configuration Request and using the values in the OpenID - * Provider Configuration Response to derive the needed - * JWK Set uri. - * @param issuer the Issuer - * @return a {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder} that will derive the - * JWK Set uri when {@link NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder#build} is - * called - * @since 6.1 - * @see JwtDecoders - */ - public static JwkSetUriReactiveJwtDecoderBuilder withIssuerLocation(String issuer) { - return new JwkSetUriReactiveJwtDecoderBuilder( - (web) -> ReactiveJwtDecoderProviderConfigurationUtils.getConfigurationForIssuerLocation(issuer, web) - .flatMap((configuration) -> { - try { - JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); - } - catch (IllegalStateException ex) { - return Mono.error(ex); - } - return Mono.just(configuration.get("jwks_uri").toString()); - }), - ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms); - } - - /** - * Use the given JWK Set - * uri to validate JWTs. - * @param jwkSetUri the JWK Set uri to use - * @return a {@link JwkSetUriReactiveJwtDecoderBuilder} for further configurations - * - * @since 5.2 - */ - public static JwkSetUriReactiveJwtDecoderBuilder withJwkSetUri(String jwkSetUri) { - return new JwkSetUriReactiveJwtDecoderBuilder(jwkSetUri); - } - - /** - * Use the given public key to validate JWTs - * @param key the public key to use - * @return a {@link PublicKeyReactiveJwtDecoderBuilder} for further configurations - * - * @since 5.2 - */ - public static PublicKeyReactiveJwtDecoderBuilder withPublicKey(RSAPublicKey key) { - return new PublicKeyReactiveJwtDecoderBuilder(key); - } - - /** - * Use the given {@code SecretKey} to validate the MAC on a JSON Web Signature (JWS). - * @param secretKey the {@code SecretKey} used to validate the MAC - * @return a {@link SecretKeyReactiveJwtDecoderBuilder} for further configurations - * - * @since 5.2 - */ - public static SecretKeyReactiveJwtDecoderBuilder withSecretKey(SecretKey secretKey) { - return new SecretKeyReactiveJwtDecoderBuilder(secretKey); - } - - /** - * Use the given {@link Function} to validate JWTs - * @param source the {@link Function} - * @return a {@link JwkSourceReactiveJwtDecoderBuilder} for further configurations - * - * @since 5.2 - */ - public static JwkSourceReactiveJwtDecoderBuilder withJwkSource(Function> source) { - return new JwkSourceReactiveJwtDecoderBuilder(source); - } - - private static JWTClaimsSet createClaimsSet(JWTProcessor jwtProcessor, - JWT parsedToken, C context) { - try { - return jwtProcessor.process(parsedToken, context); - } - catch (BadJOSEException ex) { - throw new BadJwtException("Failed to validate the token", ex); - } - catch (JOSEException ex) { - throw new JwtException("Failed to validate the token", ex); - } - } - /** * A builder for creating {@link NimbusReactiveJwtDecoder} instances based on a * JWK Set @@ -329,15 +332,11 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private static final Duration FOREVER = Duration.ofMillis(Long.MAX_VALUE); - private Function> jwkSetUri; - + private final Function> jwkSetUri; + private final Set signatureAlgorithms = new HashSet<>(); private Function>> defaultAlgorithms = (source) -> Mono - .just(Set.of(JWSAlgorithm.RS256)); - + .just(Set.of(JWSAlgorithm.RS256)); private JOSEObjectTypeVerifier typeVerifier = NO_TYPE_VERIFIER; - - private Set signatureAlgorithms = new HashSet<>(); - private WebClient webClient = WebClient.create(); private BiFunction, Mono>> jwtProcessorCustomizer; @@ -485,7 +484,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet(); if (this.signatureAlgorithms.isEmpty()) { return this.defaultAlgorithms.apply(source) - .map((algorithms) -> new JWSVerificationKeySelector<>(algorithms, jwkSource)); + .map((algorithms) -> new JWSVerificationKeySelector<>(algorithms, jwkSource)); } Set jwsAlgorithms = new HashSet<>(); for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { @@ -503,21 +502,21 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { source.setWebClient(this.webClient); Mono> jwsKeySelector = jwsKeySelector(source); Mono, Function>> jwtProcessorMono = jwsKeySelector - .flatMap((selector) -> { - jwtProcessor.setJWSKeySelector(selector); - jwtProcessor.setJWSTypeVerifier(this.typeVerifier); - return this.jwtProcessorCustomizer.apply(source, jwtProcessor); - }) - .map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector()))) - .cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO); + .flatMap((selector) -> { + jwtProcessor.setJWSKeySelector(selector); + jwtProcessor.setJWSTypeVerifier(this.typeVerifier); + return this.jwtProcessorCustomizer.apply(source, jwtProcessor); + }) + .map((processor) -> Tuples.of(processor, getExpectedJwsAlgorithms(processor.getJWSKeySelector()))) + .cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO); return (jwt) -> { return jwtProcessorMono.flatMap((tuple) -> { ConfigurableJWTProcessor processor = tuple.getT1(); Function expectedJwsAlgorithms = tuple.getT2(); JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader()); return source.get(selector) - .onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex)) - .map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList))); + .onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex)) + .map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList))); }); }; } @@ -933,9 +932,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { return (jwt) -> { if (jwt instanceof SignedJWT) { return this.jwkSource.apply((SignedJWT) jwt) - .onErrorMap((e) -> new IllegalStateException("Could not obtain the keys", e)) - .collectList() - .map((jwks) -> createClaimsSet(jwtProcessor, jwt, new JWKSecurityContext(jwks))); + .onErrorMap((e) -> new IllegalStateException("Could not obtain the keys", e)) + .collectList() + .map((jwks) -> createClaimsSet(jwtProcessor, jwt, new JWKSecurityContext(jwks))); } throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); }; 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 ca5acff2ef..0799ddc821 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 @@ -58,7 +58,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.security.oauth2.core.ReactiveOAuth2TokenValidator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -67,20 +66,14 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; +import org.springframework.security.oauth2.core.ReactiveOAuth2TokenValidator; import org.springframework.security.oauth2.jose.TestKeys; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.web.reactive.function.client.WebClient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; /** * @author Rob Winch @@ -90,14 +83,13 @@ import static org.mockito.Mockito.verify; */ public class NimbusReactiveJwtDecoderTests { - private String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg"; - - private String messageReadToken = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.bnQ8IJDXmQbmIXWku0YT1HOyV_3d0iQSA_0W2CmPyELhsxFETzBEEcZ0v0xCBiswDT51rwD83wbX3YXxb84fM64AhpU8wWOxLjha4J6HJX2JnlG47ydaAVD7eWGSYTavyyQ-CwUjQWrfMVcObFZLYG11ydzRYOR9-aiHcK3AobcTcS8jZFeI8EGQV_Cd3IJ018uFCf6VnXLv7eV2kRt08Go2RiPLW47ExvD7Dzzz_wDBKfb4pNem7fDvuzB3UPcp5m9QvLZicnbS_6AvDi6P1y_DFJf-1T5gkGmX5piDH1L1jg2Yl6tjmXbk5B3VhsyjJuXE6gzq1d-xie0Z1NVOxw"; - - private String unsignedToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; - + private static KeyFactory kf; + private final String expired = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MTUyOTkzNzYzMX0.Dt5jFOKkB8zAmjciwvlGkj4LNStXWH0HNIfr8YYajIthBIpVgY5Hg_JL8GBmUFzKDgyusT0q60OOg8_Pdi4Lu-VTWyYutLSlNUNayMlyBaVEWfyZJnh2_OwMZr1vRys6HF-o1qZldhwcfvczHg61LwPa1ISoqaAltDTzBu9cGISz2iBUCuR0x71QhbuRNyJdjsyS96NqiM_TspyiOSxmlNch2oAef1MssOQ23CrKilIvEDsz_zk5H94q7rH0giWGdEHCENESsTJS0zvzH6r2xIWjd5WnihFpCPkwznEayxaEhrdvJqT_ceyXCIfY4m3vujPQHNDG0UshpwvDuEbPUg"; + private final String messageReadToken = "eyJraWQiOiJrZXktaWQtMSIsImFsZyI6IlJTMjU2In0.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.bnQ8IJDXmQbmIXWku0YT1HOyV_3d0iQSA_0W2CmPyELhsxFETzBEEcZ0v0xCBiswDT51rwD83wbX3YXxb84fM64AhpU8wWOxLjha4J6HJX2JnlG47ydaAVD7eWGSYTavyyQ-CwUjQWrfMVcObFZLYG11ydzRYOR9-aiHcK3AobcTcS8jZFeI8EGQV_Cd3IJ018uFCf6VnXLv7eV2kRt08Go2RiPLW47ExvD7Dzzz_wDBKfb4pNem7fDvuzB3UPcp5m9QvLZicnbS_6AvDi6P1y_DFJf-1T5gkGmX5piDH1L1jg2Yl6tjmXbk5B3VhsyjJuXE6gzq1d-xie0Z1NVOxw"; + private final String unsignedToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9."; + // @formatter:on // @formatter:off - private String jwkSet = "{\n" + private final String jwkSet = "{\n" + " \"keys\":[\n" + " {\n" + " \"kty\":\"RSA\",\n" @@ -108,27 +100,29 @@ public class NimbusReactiveJwtDecoderTests { + " }\n" + " ]\n" + "}"; - // @formatter:on - - private String jwkSetUri = "https://issuer/certs"; - - private String rsa512 = "eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYxMTl9.LKAx-60EBfD7jC1jb1eKcjO4uLvf3ssISV-8tN-qp7gAjSvKvj4YA9-V2mIb6jcS1X_xGmNy6EIimZXpWaBR3nJmeu-jpe85u4WaW2Ztr8ecAi-dTO7ZozwdtljKuBKKvj4u1nF70zyCNl15AozSG0W1ASrjUuWrJtfyDG6WoZ8VfNMuhtU-xUYUFvscmeZKUYQcJ1KS-oV5tHeF8aNiwQoiPC_9KXCOZtNEJFdq6-uzFdHxvOP2yex5Gbmg5hXonauIFXG2ZPPGdXzm-5xkhBpgM8U7A_6wb3So8wBvLYYm2245QUump63AJRAy8tQpwt4n9MvQxQgS3z9R-NK92A"; - - private String rsa256 = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYzMzl9.CT-H2OWEqmSs1NWmnta5ealLFvM8OlbQTjGhfRcKLNxrTrzsOkqBJl-AN3k16BQU7mS32o744TiiZ29NcDlxPsr1MqTlN86-dobPiuNIDLp3A1bOVdXMcVFuMYkrNv0yW0tGS9OjEqsCCuZDkZ1by6AhsHLbGwRY-6AQdcRouZygGpOQu1hNun5j8q5DpSTY4AXKARIFlF-O3OpVbPJ0ebr3Ki-i3U9p_55H0e4-wx2bqcApWlqgofl1I8NKWacbhZgn81iibup2W7E0CzCzh71u1Mcy3xk1sYePx-dwcxJnHmxJReBBWjJZEAeCrkbnn_OCuo2fA-EQyNJtlN5F2w"; - - private String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq4yKxb6SNePdDmQi9xFCrP6QvHosErQzryknQTTTffs0t3cy3Er3lIceuhZ7yQNSCDfPFqG8GoyoKhuChRiA5D+J2ab7bqTa1QJKfnCyERoscftgN2fXPHjHoiKbpGV2tMVw8mXl//tePOAiKbMJaBUnlAvJgkk1rVm08dSwpLC1sr2M19euf9jwnRGkMRZuhp9iCPgECRke5T8Ixpv0uQjSmGHnWUKTFlbj8sM83suROR1Ue64JSGScANc5vk3huJ/J97qTC+K2oKj6L8d9O8dpc4obijEOJwpydNvTYDgbiivYeSB00KS9jlBkQ5B2QqLvLVEygDl3dp59nGx6YQIDAQAB"; - + private final String jwkSetUri = "https://issuer/certs"; + private final String rsa512 = "eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYxMTl9.LKAx-60EBfD7jC1jb1eKcjO4uLvf3ssISV-8tN-qp7gAjSvKvj4YA9-V2mIb6jcS1X_xGmNy6EIimZXpWaBR3nJmeu-jpe85u4WaW2Ztr8ecAi-dTO7ZozwdtljKuBKKvj4u1nF70zyCNl15AozSG0W1ASrjUuWrJtfyDG6WoZ8VfNMuhtU-xUYUFvscmeZKUYQcJ1KS-oV5tHeF8aNiwQoiPC_9KXCOZtNEJFdq6-uzFdHxvOP2yex5Gbmg5hXonauIFXG2ZPPGdXzm-5xkhBpgM8U7A_6wb3So8wBvLYYm2245QUump63AJRAy8tQpwt4n9MvQxQgS3z9R-NK92A"; + private final String rsa256 = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJleHAiOjE5NzQzMjYzMzl9.CT-H2OWEqmSs1NWmnta5ealLFvM8OlbQTjGhfRcKLNxrTrzsOkqBJl-AN3k16BQU7mS32o744TiiZ29NcDlxPsr1MqTlN86-dobPiuNIDLp3A1bOVdXMcVFuMYkrNv0yW0tGS9OjEqsCCuZDkZ1by6AhsHLbGwRY-6AQdcRouZygGpOQu1hNun5j8q5DpSTY4AXKARIFlF-O3OpVbPJ0ebr3Ki-i3U9p_55H0e4-wx2bqcApWlqgofl1I8NKWacbhZgn81iibup2W7E0CzCzh71u1Mcy3xk1sYePx-dwcxJnHmxJReBBWjJZEAeCrkbnn_OCuo2fA-EQyNJtlN5F2w"; + private final String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq4yKxb6SNePdDmQi9xFCrP6QvHosErQzryknQTTTffs0t3cy3Er3lIceuhZ7yQNSCDfPFqG8GoyoKhuChRiA5D+J2ab7bqTa1QJKfnCyERoscftgN2fXPHjHoiKbpGV2tMVw8mXl//tePOAiKbMJaBUnlAvJgkk1rVm08dSwpLC1sr2M19euf9jwnRGkMRZuhp9iCPgECRke5T8Ixpv0uQjSmGHnWUKTFlbj8sM83suROR1Ue64JSGScANc5vk3huJ/J97qTC+K2oKj6L8d9O8dpc4obijEOJwpydNvTYDgbiivYeSB00KS9jlBkQ5B2QqLvLVEygDl3dp59nGx6YQIDAQAB"; private MockWebServer server; - private NimbusReactiveJwtDecoder decoder; - private static KeyFactory kf; - @BeforeAll public static void keyFactory() throws NoSuchAlgorithmException { kf = KeyFactory.getInstance("RSA"); } + private static WebClient mockJwkSetResponse(String response) { + WebClient real = WebClient.builder().build(); + WebClient.RequestHeadersUriSpec spec = spy(real.get()); + WebClient webClient = spy(WebClient.class); + given(webClient.get()).willReturn(spec); + WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); + given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(response)); + given(spec.retrieve()).willReturn(responseSpec); + return webClient; + } + @BeforeEach public void setup() throws Exception { this.server = new MockWebServer(); @@ -161,9 +155,9 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenRSAPublicKeyThenSuccess() throws Exception { byte[] bytes = Base64.getDecoder() - .decode("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqL48v1clgFw+Evm145pmh8nRYiNt72Gupsshn7Qs8dxEydCRp1DPOV/PahPk1y2nvldBNIhfNL13JOAiJ6BTiF+2ICuICAhDArLMnTH61oL1Hepq8W1xpa9gxsnL1P51thvfmiiT4RTW57koy4xIWmIp8ZXXfYgdH2uHJ9R0CQBuYKe7nEOObjxCFWC8S30huOfW2cYtv0iB23h6w5z2fDLjddX6v/FXM7ktcokgpm3/XmvT/+bL6/GGwz9k6kJOyMTubecr+WT//le8ikY66zlplYXRQh6roFfFCL21Pt8xN5zrk+0AMZUnmi8F2S2ztSBmAVJ7H71ELXsURBVZpwIDAQAB"); + .decode("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqL48v1clgFw+Evm145pmh8nRYiNt72Gupsshn7Qs8dxEydCRp1DPOV/PahPk1y2nvldBNIhfNL13JOAiJ6BTiF+2ICuICAhDArLMnTH61oL1Hepq8W1xpa9gxsnL1P51thvfmiiT4RTW57koy4xIWmIp8ZXXfYgdH2uHJ9R0CQBuYKe7nEOObjxCFWC8S30huOfW2cYtv0iB23h6w5z2fDLjddX6v/FXM7ktcokgpm3/XmvT/+bL6/GGwz9k6kJOyMTubecr+WT//le8ikY66zlplYXRQh6roFfFCL21Pt8xN5zrk+0AMZUnmi8F2S2ztSBmAVJ7H71ELXsURBVZpwIDAQAB"); RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(bytes)); + .generatePublic(new X509EncodedKeySpec(bytes)); this.decoder = new NimbusReactiveJwtDecoder(publicKey); String noKeyId = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6IiIsImV4cCI6OTIyMzM3MjAwNjA5NjM3NX0.hNVuHSUkxdLZrDfqdmKcOi0ggmNaDuB4ZPxPtJl1gwBiXzIGN6Hwl24O2BfBZiHFKUTQDs4_RvzD71mEG3DvUrcKmdYWqIB1l8KNmxQLUDG-cAPIpJmRJgCh50tf8OhOE_Cb9E1HcsOUb47kT9iz-VayNBcmo6BmyZLdEGhsdGBrc3Mkz2dd_0PF38I2Hf_cuSjn9gBjFGtiPEXJvob3PEjVTSx_zvodT8D9p3An1R3YBZf5JSd1cQisrXgDX2k1Jmf7UKKWzgfyCgnEtRWWbsUdPqo3rSEY9GDC1iSQXsFTTC1FT_JJDkwzGf011fsU5O_Ko28TARibmKTCxAKNRQ"; this.decoder.decode(noKeyId).block(); @@ -179,7 +173,7 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenExpiredThenFail() { assertThatExceptionOfType(JwtValidationException.class) - .isThrownBy(() -> this.decoder.decode(this.expired).block()); + .isThrownBy(() -> this.decoder.decode(this.expired).block()); } @Test @@ -203,7 +197,7 @@ public class NimbusReactiveJwtDecoderTests { public void decodeWhenInvalidSignatureThenFail() { assertThatExceptionOfType(BadJwtException.class).isThrownBy( () -> this.decoder.decode(this.messageReadToken.substring(0, this.messageReadToken.length() - 2)) - .block()); + .block()); } @Test @@ -315,7 +309,7 @@ public class NimbusReactiveJwtDecoderTests { @Test public void jwsAlgorithmWhenNullThenThrowsException() { NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder - .withJwkSetUri(this.jwkSetUri); + .withJwkSetUri(this.jwkSetUri); assertThatIllegalArgumentException().isThrownBy(() -> builder.jwsAlgorithm(null)); } @@ -335,7 +329,7 @@ public class NimbusReactiveJwtDecoderTests { @Test public void restOperationsWhenNullThenThrowsException() { NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder - .withJwkSetUri(this.jwkSetUri); + .withJwkSetUri(this.jwkSetUri); // @formatter:off assertThatIllegalArgumentException() .isThrownBy(() -> builder.webClient(null)); @@ -482,10 +476,10 @@ public class NimbusReactiveJwtDecoderTests { @Test public void withJwkSourceWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { assertThatIllegalArgumentException() - .isThrownBy(() -> NimbusReactiveJwtDecoder.withJwkSource((jwt) -> Flux.empty()) - .jwtProcessorCustomizer(null) - .build()) - .withMessage("jwtProcessorCustomizer cannot be null"); + .isThrownBy(() -> NimbusReactiveJwtDecoder.withJwkSource((jwt) -> Flux.empty()) + .jwtProcessorCustomizer(null) + .build()) + .withMessage("jwtProcessorCustomizer cannot be null"); } @Test @@ -595,8 +589,8 @@ public class NimbusReactiveJwtDecoderTests { SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY; MacAlgorithm macAlgorithm = MacAlgorithm.HS256; JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().subject("test-subject") - .expirationTime(Date.from(Instant.now().plusSeconds(60))) - .build(); + .expirationTime(Date.from(Instant.now().plusSeconds(60))) + .build(); SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet); // @formatter:off this.decoder = NimbusReactiveJwtDecoder.withSecretKey(secretKey) @@ -617,11 +611,11 @@ public class NimbusReactiveJwtDecoderTests { WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet)); given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class))) - .willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"))); + .willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"))); given(spec.retrieve()).willReturn(responseSpec); ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer) - .webClient(webClient) - .build(); + .webClient(webClient) + .build(); Jwt jwt = jwtDecoder.decode(this.messageReadToken).block(); assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull(); } @@ -630,8 +624,8 @@ public class NimbusReactiveJwtDecoderTests { public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() { ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class); JWSKeySelector jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsKeySelector(jwkSource) - .block(); + .jwsKeySelector(jwkSource) + .block(); assertThat(jwsKeySelector).isInstanceOf(JWSVerificationKeySelector.class); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)).isTrue(); @@ -641,9 +635,9 @@ public class NimbusReactiveJwtDecoderTests { public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() { ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class); JWSKeySelector jwsKeySelector = NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) - .jwsAlgorithm(SignatureAlgorithm.RS512) - .jwsKeySelector(jwkSource) - .block(); + .jwsAlgorithm(SignatureAlgorithm.RS512) + .jwsKeySelector(jwkSource) + .block(); assertThat(jwsKeySelector).isInstanceOf(JWSVerificationKeySelector.class); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS512)).isTrue(); @@ -667,8 +661,8 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenPublicKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception { NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY) - .validateType(false) - .build(); + .validateType(false) + .build(); jwtDecoder.setJwtValidator((OAuth2TokenValidator) (jwt) -> OAuth2TokenValidatorResult.success()); RSAPrivateKey privateKey = TestKeys.DEFAULT_PRIVATE_KEY; SignedJWT jwt = signedJwt(privateKey, @@ -680,8 +674,8 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenSecretKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception { NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY) - .validateType(false) - .build(); + .validateType(false) + .build(); jwtDecoder.setJwtValidator((OAuth2TokenValidator) (jwt) -> OAuth2TokenValidatorResult.success()); SignedJWT jwt = signedJwt(TestKeys.DEFAULT_SECRET_KEY, new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JOSE).build(), @@ -692,11 +686,11 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenJwkSourceValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception { JWK jwk = new RSAKey.Builder(TestKeys.DEFAULT_PUBLIC_KEY).privateKey(TestKeys.DEFAULT_PRIVATE_KEY) - .algorithm(JWSAlgorithm.RS256) - .build(); + .algorithm(JWSAlgorithm.RS256) + .build(); NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSource((jwt) -> Flux.just(jwk)) - .validateType(false) - .build(); + .validateType(false) + .build(); jwtDecoder.setJwtValidator((OAuth2TokenValidator) (jwt) -> OAuth2TokenValidatorResult.success()); SignedJWT jwt = signedJwt(TestKeys.DEFAULT_PRIVATE_KEY, new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JOSE).build(), @@ -740,15 +734,4 @@ public class NimbusReactiveJwtDecoderTests { return (RSAPublicKey) kf.generatePublic(spec); } - private static WebClient mockJwkSetResponse(String response) { - WebClient real = WebClient.builder().build(); - WebClient.RequestHeadersUriSpec spec = spy(real.get()); - WebClient webClient = spy(WebClient.class); - given(webClient.get()).willReturn(spec); - WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); - given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(response)); - given(spec.retrieve()).willReturn(responseSpec); - return webClient; - } - }