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..8496c51280 --- /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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; + +/** + * 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(Collections.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), Collections.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..ca5f2ef9e5 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ReactiveWrappingOAuth2TokenValidator.java @@ -0,0 +1,46 @@ +/* + * 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; + +import org.springframework.util.Assert; + +/** + * 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..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 @@ -29,6 +29,8 @@ 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; @@ -37,6 +39,7 @@ import org.springframework.util.CollectionUtils; * * @author Josh Cummings * @author Rob Winch + * @author Iain Henderson * @since 5.1 */ public final class JwtValidators { @@ -71,8 +74,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 +82,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 4e103f2daa..31df118e26 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 @@ -64,7 +64,8 @@ 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.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,10 +98,10 @@ 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()); + .withDefaults(Collections.emptyMap()); /** * Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters. @@ -129,90 +130,6 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { this.jwtProcessor = jwtProcessor; } - /** - * Use the provided {@link OAuth2TokenValidator} to validate incoming {@link Jwt}s. - * @param jwtValidator the {@link OAuth2TokenValidator} to use - */ - public void setJwtValidator(OAuth2TokenValidator jwtValidator) { - Assert.notNull(jwtValidator, "jwtValidator cannot be null"); - this.jwtValidator = jwtValidator; - } - - /** - * Use the following {@link Converter} for manipulating the JWT's claim set - * @param claimSetConverter the {@link Converter} to use - */ - public void setClaimSetConverter(Converter, Map> claimSetConverter) { - Assert.notNull(claimSetConverter, "claimSetConverter cannot be null"); - this.claimSetConverter = claimSetConverter; - } - - @Override - public Mono decode(String token) { - try { - JWT jwt = JWTParser.parse(token); - if (jwt instanceof PlainJWT) { - return Mono.error(new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm())); - } - return this.decode(jwt); - } - catch (Exception ex) { - return Mono.error(new BadJwtException( - "An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex)); - } - } - - private Mono decode(JWT parsedToken) { - try { - // @formatter:off - return this.jwtProcessor.convert(parsedToken) - .map((set) -> createJwt(parsedToken, set)) - .map(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 - } - catch (JwtException ex) { - throw ex; - } - catch (RuntimeException ex) { - throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); - } - } - - private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { - try { - 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(); - } - catch (Exception ex) { - throw new BadJwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); - } - } - - 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 String getJwtValidationExceptionMessage(Collection errors) { - for (OAuth2Error oAuth2Error : errors) { - if (StringUtils.hasLength(oAuth2Error.getDescription())) { - return oAuth2Error.getDescription(); - } - } - return "Unable to validate Jwt"; - } - /** * Use the given Issuer @@ -305,6 +222,102 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { } } + /** + * Use the provided {@link OAuth2TokenValidator} to validate incoming {@link Jwt}s. + * @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; + } + + /** + * Use the following {@link Converter} for manipulating the JWT's claim set + * @param claimSetConverter the {@link Converter} to use + */ + public void setClaimSetConverter(Converter, Map> claimSetConverter) { + Assert.notNull(claimSetConverter, "claimSetConverter cannot be null"); + this.claimSetConverter = claimSetConverter; + } + + @Override + public Mono decode(String token) { + try { + JWT jwt = JWTParser.parse(token); + if (jwt instanceof PlainJWT) { + return Mono.error(new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm())); + } + return this.decode(jwt); + } + catch (Exception ex) { + return Mono.error(new BadJwtException( + "An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex)); + } + } + + private Mono decode(JWT parsedToken) { + try { + // @formatter:off + return this.jwtProcessor.convert(parsedToken) + .map((set) -> createJwt(parsedToken, set)) + .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 + } + catch (JwtException ex) { + throw ex; + } + catch (RuntimeException ex) { + throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + private Jwt createJwt(JWT parsedJwt, JWTClaimsSet jwtClaimsSet) { + try { + 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(); + } + catch (Exception ex) { + throw new BadJwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex); + } + } + + 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) { + for (OAuth2Error oAuth2Error : errors) { + if (StringUtils.hasLength(oAuth2Error.getDescription())) { + return oAuth2Error.getDescription(); + } + } + return "Unable to validate Jwt"; + } + /** * A builder for creating {@link NimbusReactiveJwtDecoder} instances based on a * JWK Set @@ -322,15 +335,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; @@ -478,7 +487,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) { @@ -496,21 +505,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))); }); }; } @@ -926,9 +935,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 beb5966b9b..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 @@ -66,36 +66,30 @@ 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 * @author Joe Grandja + * @author Iain Henderson * @since 5.1 */ 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" @@ -106,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(); @@ -159,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(); @@ -177,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 @@ -201,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 @@ -293,7 +289,7 @@ public class NimbusReactiveJwtDecoderTests { public void setJwtValidatorWhenGivenNullThrowsIllegalArgumentException() { // @formatter:off assertThatIllegalArgumentException() - .isThrownBy(() -> this.decoder.setJwtValidator(null)); + .isThrownBy(() -> this.decoder.setJwtValidator((ReactiveOAuth2TokenValidator) null)); // @formatter:on } @@ -313,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)); } @@ -333,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)); @@ -480,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 @@ -593,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) @@ -615,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(); } @@ -628,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(); @@ -639,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(); @@ -665,9 +661,9 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenPublicKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception { NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(TestKeys.DEFAULT_PUBLIC_KEY) - .validateType(false) - .build(); - jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success()); + .validateType(false) + .build(); + 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(), @@ -678,9 +674,9 @@ public class NimbusReactiveJwtDecoderTests { @Test public void decodeWhenSecretKeyValidateTypeFalseThenSkipsNimbusTypeValidation() throws Exception { NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withSecretKey(TestKeys.DEFAULT_SECRET_KEY) - .validateType(false) - .build(); - jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success()); + .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(), new JWTClaimsSet.Builder().subject("subject").build()); @@ -690,12 +686,12 @@ 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(); - jwtDecoder.setJwtValidator((jwt) -> OAuth2TokenValidatorResult.success()); + .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(), new JWTClaimsSet.Builder().subject("subject").build()); @@ -738,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; - } - }