From e1bae73703ea36369927d943b85d768c47d88ea4 Mon Sep 17 00:00:00 2001 From: Baljit Singh Date: Thu, 13 Jul 2023 11:03:31 -0400 Subject: [PATCH] add factory methods for Jwt issuer resolvers Closes gh-13427 --- ...wtIssuerAuthenticationManagerResolver.java | 34 +++ ...ReactiveAuthenticationManagerResolver.java | 34 +++ ...icationManagerResolverDeprecatedTests.java | 255 ++++++++++++++++++ ...uerAuthenticationManagerResolverTests.java | 31 ++- ...icationManagerResolverDeprecatedTests.java | 254 +++++++++++++++++ ...iveAuthenticationManagerResolverTests.java | 35 +-- 6 files changed, 613 insertions(+), 30 deletions(-) create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java create mode 100644 oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java index 1d2e409533..6f12c0e8ef 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java @@ -67,7 +67,9 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided * parameters * @param trustedIssuers an array of trusted issuers + * @deprecated use {@link #fromTrustedIssuers(String...)} */ + @Deprecated(since = "6.2", forRemoval = true) public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) { this(Set.of(trustedIssuers)); } @@ -76,13 +78,45 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided * parameters * @param trustedIssuers a collection of trusted issuers + * @deprecated use {@link #fromTrustedIssuers(Collection)} */ + @Deprecated(since = "6.2", forRemoval = true) public JwtIssuerAuthenticationManagerResolver(Collection trustedIssuers) { Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); this.authenticationManager = new ResolvingAuthenticationManager( new TrustedIssuerJwtAuthenticationManagerResolver(Set.copyOf(trustedIssuers)::contains)); } + /** + * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided + * parameters + * @param trustedIssuers an array of trusted issuers + */ + public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(String... trustedIssuers) { + return fromTrustedIssuers(Set.of(trustedIssuers)); + } + + /** + * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided + * parameters + * @param trustedIssuers a collection of trusted issuers + */ + public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(Collection trustedIssuers) { + Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); + return fromTrustedIssuers(Set.copyOf(trustedIssuers)::contains); + } + + /** + * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided + * parameters + * @param trustedIssuers a predicate to validate issuers + */ + public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(Predicate trustedIssuers) { + Assert.notNull(trustedIssuers, "trustedIssuers cannot be null"); + return new JwtIssuerAuthenticationManagerResolver( + new TrustedIssuerJwtAuthenticationManagerResolver(trustedIssuers)); + } + /** * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided * parameters diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java index a19a2a6c4f..d8e7a6ce90 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java @@ -71,7 +71,9 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the * provided parameters * @param trustedIssuers an array of trusted issuers + * @deprecated use {@link #fromTrustedIssuers(String...)} */ + @Deprecated(since = "6.2", forRemoval = true) public JwtIssuerReactiveAuthenticationManagerResolver(String... trustedIssuers) { this(Set.of(trustedIssuers)); } @@ -80,13 +82,45 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the * provided parameters * @param trustedIssuers a collection of trusted issuers + * @deprecated use {@link #fromTrustedIssuers(Collection)} */ + @Deprecated(since = "6.2", forRemoval = true) public JwtIssuerReactiveAuthenticationManagerResolver(Collection trustedIssuers) { Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); this.authenticationManager = new ResolvingAuthenticationManager( new TrustedIssuerJwtAuthenticationManagerResolver(Set.copyOf(trustedIssuers)::contains)); } + /** + * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the + * provided parameters + * @param trustedIssuers an array of trusted issuers + */ + public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(String... trustedIssuers) { + return fromTrustedIssuers(Set.of(trustedIssuers)); + } + + /** + * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the + * provided parameters + * @param trustedIssuers a collection of trusted issuers + */ + public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(Collection trustedIssuers) { + Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); + return fromTrustedIssuers(Set.copyOf(trustedIssuers)::contains); + } + + /** + * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the + * provided parameters + * @param trustedIssuers a predicate to validate issuers + */ + public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(Predicate trustedIssuers) { + Assert.notNull(trustedIssuers, "trustedIssuers cannot be null"); + return new JwtIssuerReactiveAuthenticationManagerResolver( + new TrustedIssuerJwtAuthenticationManagerResolver(trustedIssuers)); + } + /** * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the * provided parameters diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java new file mode 100644 index 0000000000..478cb31d8d --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverDeprecatedTests.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2020 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.server.resource.authentication; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; +import net.minidev.json.JSONObject; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.Test; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jose.TestKeys; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver; + +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.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; + +/** + * Tests for {@link JwtIssuerAuthenticationManagerResolver} + */ +@Deprecated +public class JwtIssuerAuthenticationManagerResolverDeprecatedTests { + + private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n" + " \"issuer\": \"%s\", \n" + + " \"jwks_uri\": \"%s/.well-known/jwks.json\" \n" + "}"; + + private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw\"}]}"; + + private String jwt = jwt("iss", "trusted"); + + private String evil = jwt("iss", "\""); + + private String noIssuer = jwt("sub", "sub"); + + @Test + public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + String issuer = server.url("").toString(); + // @formatter:off + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer) + )); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(JWK_SET) + ); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(JWK_SET) + ); + // @formatter:on + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + issuer); + Authentication token = withBearerToken(jws.serialize()); + AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null); + assertThat(authenticationManager).isNotNull(); + Authentication authentication = authenticationManager.authenticate(token); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + + @Test + public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + String issuer = server.url("").toString(); + // @formatter:off + server.enqueue(new MockResponse().setResponseCode(500) + .setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)) + ); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)) + ); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(JWK_SET) + ); + // @formatter:on + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + issuer); + Authentication token = withBearerToken(jws.serialize()); + AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null); + assertThat(authenticationManager).isNotNull(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> authenticationManager.authenticate(token)); + Authentication authentication = authenticationManager.authenticate(token); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + + @Test + public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + String issuer = server.url("").toString(); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(JWK_SET)); + TrustedIssuerJwtAuthenticationManagerResolver resolver = new TrustedIssuerJwtAuthenticationManagerResolver( + (iss) -> iss.equals(issuer)); + AuthenticationManager authenticationManager = resolver.resolve(issuer); + AuthenticationManager cachedAuthenticationManager = resolver.resolve(issuer); + assertThat(authenticationManager).isSameAs(cachedAuthenticationManager); + } + } + + @Test + public void resolveWhenUsingUntrustedIssuerThenException() { + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + "other", "issuers"); + Authentication token = withBearerToken(this.jwt); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token)) + .withMessageContaining("Invalid issuer"); + // @formatter:on + } + + @Test + public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() { + AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + (issuer) -> authenticationManager); + Authentication token = withBearerToken(this.jwt); + authenticationManagerResolver.resolve(null).authenticate(token); + verify(authenticationManager).authenticate(token); + } + + @Test + public void resolveWhenUsingExternalSourceThenRespondsToChanges() { + Authentication token = withBearerToken(this.jwt); + Map authenticationManagers = new HashMap<>(); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + authenticationManagers::get); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token)) + .withMessageContaining("Invalid issuer"); + // @formatter:on + AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + authenticationManagers.put("trusted", authenticationManager); + authenticationManagerResolver.resolve(null).authenticate(token); + verify(authenticationManager).authenticate(token); + authenticationManagers.clear(); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token)) + .withMessageContaining("Invalid issuer"); + // @formatter:on + } + + @Test + public void resolveWhenBearerTokenMalformedThenException() { + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + "trusted"); + Authentication token = withBearerToken("jwt"); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token)) + .withMessageNotContaining("Invalid issuer"); + // @formatter:on + } + + @Test + public void resolveWhenBearerTokenNoIssuerThenException() { + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + "trusted"); + Authentication token = withBearerToken(this.noIssuer); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null).authenticate(token)) + .withMessageContaining("Missing issuer"); + // @formatter:on + } + + @Test + public void resolveWhenBearerTokenEvilThenGenericException() { + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + "trusted"); + Authentication token = withBearerToken(this.evil); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver + .resolve(null).authenticate(token) + ) + .withMessage("Invalid issuer"); + // @formatter:on + } + + @Test + public void constructorWhenNullOrEmptyIssuersThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver((Collection) null)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver(Collections.emptyList())); + } + + @Test + public void constructorWhenNullAuthenticationManagerResolverThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver((AuthenticationManagerResolver) null)); + } + + private Authentication withBearerToken(String token) { + return new BearerTokenAuthenticationToken(token); + } + + private String jwt(String claim, String value) { + PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build()); + return jwt.serialize(); + } + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java index 3826a922e8..bc2b1a2f21 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.Predicate; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; @@ -85,8 +86,8 @@ public class JwtIssuerAuthenticationManagerResolverTests { JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); - JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( - issuer); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver + .fromTrustedIssuers(issuer); Authentication token = withBearerToken(jws.serialize()); AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null); assertThat(authenticationManager).isNotNull(); @@ -117,8 +118,8 @@ public class JwtIssuerAuthenticationManagerResolverTests { JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); - JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( - issuer); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver + .fromTrustedIssuers(issuer); Authentication token = withBearerToken(jws.serialize()); AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null); assertThat(authenticationManager).isNotNull(); @@ -147,8 +148,8 @@ public class JwtIssuerAuthenticationManagerResolverTests { @Test public void resolveWhenUsingUntrustedIssuerThenException() { - JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( - "other", "issuers"); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver + .fromTrustedIssuers("other", "issuers"); Authentication token = withBearerToken(this.jwt); // @formatter:off assertThatExceptionOfType(OAuth2AuthenticationException.class) @@ -192,8 +193,8 @@ public class JwtIssuerAuthenticationManagerResolverTests { @Test public void resolveWhenBearerTokenMalformedThenException() { - JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( - "trusted"); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver + .fromTrustedIssuers("trusted"); Authentication token = withBearerToken("jwt"); // @formatter:off assertThatExceptionOfType(OAuth2AuthenticationException.class) @@ -204,8 +205,8 @@ public class JwtIssuerAuthenticationManagerResolverTests { @Test public void resolveWhenBearerTokenNoIssuerThenException() { - JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( - "trusted"); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver + .fromTrustedIssuers("trusted"); Authentication token = withBearerToken(this.noIssuer); // @formatter:off assertThatExceptionOfType(OAuth2AuthenticationException.class) @@ -216,8 +217,8 @@ public class JwtIssuerAuthenticationManagerResolverTests { @Test public void resolveWhenBearerTokenEvilThenGenericException() { - JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( - "trusted"); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerAuthenticationManagerResolver + .fromTrustedIssuers("trusted"); Authentication token = withBearerToken(this.evil); // @formatter:off assertThatExceptionOfType(OAuth2AuthenticationException.class) @@ -231,9 +232,11 @@ public class JwtIssuerAuthenticationManagerResolverTests { @Test public void constructorWhenNullOrEmptyIssuersThenException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver((Collection) null)); + .isThrownBy(() -> JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers((Predicate) null)); assertThatIllegalArgumentException() - .isThrownBy(() -> new JwtIssuerAuthenticationManagerResolver(Collections.emptyList())); + .isThrownBy(() -> JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers((Collection) null)); + assertThatIllegalArgumentException() + .isThrownBy(() -> JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers(Collections.emptyList())); } @Test diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java new file mode 100644 index 0000000000..507feb7fae --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2020 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.server.resource.authentication; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; +import net.minidev.json.JSONObject; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.jose.TestKeys; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerReactiveAuthenticationManagerResolver.TrustedIssuerJwtAuthenticationManagerResolver; + +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.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; + +/** + * Tests for {@link JwtIssuerReactiveAuthenticationManagerResolver} + */ +@Deprecated +public class JwtIssuerReactiveAuthenticationManagerResolverDeprecatedTests { + + // @formatter:off + private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n" + + " \"issuer\": \"%s\", \n" + + " \"jwks_uri\": \"%s/.well-known/jwks.json\" \n" + + "}"; + // @formatter:on + + private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"3FlqJr5TRskIQIgdE3Dd7D9lboWdcTUT8a-fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRvc5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4_1tfRgG6ii4Uhxh6iI8qNMJQX-fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2kJdJ_ZIV-WW4noDdzpKqHcwmB8FsrumlVY_DNVvUSDIipiq9PbP4H99TXN1o746oRaNa07rq1hoCgMSSy-85SagCoxlmyE-D-of9SsMY8Ol9t0rdzpobBuhyJ_o5dfvjKw\"}]}"; + + private String jwt = jwt("iss", "trusted"); + + private String evil = jwt("iss", "\""); + + private String noIssuer = jwt("sub", "sub"); + + @Test + public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + String issuer = server.url("").toString(); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(JWK_SET)); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(JWK_SET)); + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + issuer); + ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block(); + assertThat(authenticationManager).isNotNull(); + BearerTokenAuthenticationToken token = withBearerToken(jws.serialize()); + Authentication authentication = authenticationManager.authenticate(token).block(); + assertThat(authentication).isNotNull(); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + + // gh-10444 + @Test + public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + String issuer = server.url("").toString(); + // @formatter:off + server.enqueue(new MockResponse().setResponseCode(500).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(JWK_SET)); + // @formatter:on + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + issuer); + ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block(); + assertThat(authenticationManager).isNotNull(); + Authentication token = withBearerToken(jws.serialize()); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> authenticationManager.authenticate(token).block()); + Authentication authentication = authenticationManager.authenticate(token).block(); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + + @Test + public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + String issuer = server.url("").toString(); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(JWK_SET)); + TrustedIssuerJwtAuthenticationManagerResolver resolver = new TrustedIssuerJwtAuthenticationManagerResolver( + (iss) -> iss.equals(issuer)); + ReactiveAuthenticationManager authenticationManager = resolver.resolve(issuer).block(); + ReactiveAuthenticationManager cachedAuthenticationManager = resolver.resolve(issuer).block(); + assertThat(authenticationManager).isSameAs(cachedAuthenticationManager); + } + } + + @Test + public void resolveWhenUsingUntrustedIssuerThenException() { + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + "other", "issuers"); + Authentication token = withBearerToken(this.jwt); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null) + .flatMap((authenticationManager) -> authenticationManager.authenticate(token)) + .block()) + .withMessageContaining("Invalid issuer"); + // @formatter:on + } + + @Test + public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() { + Authentication token = withBearerToken(this.jwt); + ReactiveAuthenticationManager authenticationManager = mock(ReactiveAuthenticationManager.class); + given(authenticationManager.authenticate(token)).willReturn(Mono.empty()); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + (issuer) -> Mono.just(authenticationManager)); + authenticationManagerResolver.resolve(null).flatMap((manager) -> manager.authenticate(token)).block(); + verify(authenticationManager).authenticate(any()); + } + + @Test + public void resolveWhenUsingExternalSourceThenRespondsToChanges() { + Authentication token = withBearerToken(this.jwt); + Map authenticationManagers = new HashMap<>(); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + (issuer) -> Mono.justOrEmpty(authenticationManagers.get(issuer))); + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null) + .flatMap((manager) -> manager.authenticate(token)).block()) + .withMessageContaining("Invalid issuer"); + ReactiveAuthenticationManager authenticationManager = mock(ReactiveAuthenticationManager.class); + given(authenticationManager.authenticate(token)).willReturn(Mono.empty()); + authenticationManagers.put("trusted", authenticationManager); + authenticationManagerResolver.resolve(null).flatMap((manager) -> manager.authenticate(token)).block(); + verify(authenticationManager).authenticate(token); + authenticationManagers.clear(); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null) + .flatMap((manager) -> manager.authenticate(token)) + .block()) + .withMessageContaining("Invalid issuer"); + // @formatter:on + } + + @Test + public void resolveWhenBearerTokenMalformedThenException() { + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + "trusted"); + Authentication token = withBearerToken("jwt"); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null) + .flatMap((manager) -> manager.authenticate(token)) + .block()) + .withMessageNotContaining("Invalid issuer"); + // @formatter:on + } + + @Test + public void resolveWhenBearerTokenNoIssuerThenException() { + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + "trusted"); + Authentication token = withBearerToken(this.noIssuer); + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null) + .flatMap((manager) -> manager.authenticate(token)).block()) + .withMessageContaining("Missing issuer"); + } + + @Test + public void resolveWhenBearerTokenEvilThenGenericException() { + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + "trusted"); + Authentication token = withBearerToken(this.evil); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> authenticationManagerResolver.resolve(null) + .flatMap((manager) -> manager.authenticate(token)) + .block()) + .withMessage("Invalid token"); + // @formatter:on + } + + @Test + public void constructorWhenNullOrEmptyIssuersThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver((Collection) null)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver(Collections.emptyList())); + } + + @Test + public void constructorWhenNullAuthenticationManagerResolverThenException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new JwtIssuerReactiveAuthenticationManagerResolver((ReactiveAuthenticationManagerResolver) null)); + } + + private String jwt(String claim, String value) { + PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build()); + return jwt.serialize(); + } + + private BearerTokenAuthenticationToken withBearerToken(String token) { + return new BearerTokenAuthenticationToken(token); + } + +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java index 7b5851018d..ee21661321 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.Predicate; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; @@ -83,8 +84,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); - JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( - issuer); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver + .fromTrustedIssuers(issuer); ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block(); assertThat(authenticationManager).isNotNull(); BearerTokenAuthenticationToken token = withBearerToken(jws.serialize()); @@ -110,8 +111,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); - JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( - issuer); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver + .fromTrustedIssuers(issuer); ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block(); assertThat(authenticationManager).isNotNull(); Authentication token = withBearerToken(jws.serialize()); @@ -140,8 +141,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { @Test public void resolveWhenUsingUntrustedIssuerThenException() { - JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( - "other", "issuers"); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver + .fromTrustedIssuers("other", "issuers"); Authentication token = withBearerToken(this.jwt); // @formatter:off assertThatExceptionOfType(OAuth2AuthenticationException.class) @@ -190,8 +191,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { @Test public void resolveWhenBearerTokenMalformedThenException() { - JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( - "trusted"); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver + .fromTrustedIssuers("trusted"); Authentication token = withBearerToken("jwt"); // @formatter:off assertThatExceptionOfType(OAuth2AuthenticationException.class) @@ -204,8 +205,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { @Test public void resolveWhenBearerTokenNoIssuerThenException() { - JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( - "trusted"); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver + .fromTrustedIssuers("trusted"); Authentication token = withBearerToken(this.noIssuer); assertThatExceptionOfType(OAuth2AuthenticationException.class) .isThrownBy(() -> authenticationManagerResolver.resolve(null) @@ -215,8 +216,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { @Test public void resolveWhenBearerTokenEvilThenGenericException() { - JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( - "trusted"); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver + .fromTrustedIssuers("trusted"); Authentication token = withBearerToken(this.evil); // @formatter:off assertThatExceptionOfType(OAuth2AuthenticationException.class) @@ -229,10 +230,12 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { @Test public void constructorWhenNullOrEmptyIssuersThenException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver((Collection) null)); - assertThatIllegalArgumentException() - .isThrownBy(() -> new JwtIssuerReactiveAuthenticationManagerResolver(Collections.emptyList())); + assertThatIllegalArgumentException().isThrownBy( + () -> JwtIssuerReactiveAuthenticationManagerResolver.fromTrustedIssuers((Predicate) null)); + assertThatIllegalArgumentException().isThrownBy( + () -> JwtIssuerReactiveAuthenticationManagerResolver.fromTrustedIssuers((Collection) null)); + assertThatIllegalArgumentException().isThrownBy( + () -> JwtIssuerReactiveAuthenticationManagerResolver.fromTrustedIssuers(Collections.emptyList())); } @Test