浏览代码

Add Support for JWK Signature Algorithm Discovery

Issue gh-7160
Nick Hitchan 5 年之前
父节点
当前提交
290786438c
共有 13 个文件被更改,包括 229 次插入12 次删除
  1. 4 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java
  2. 4 0
      config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java
  3. 2 0
      config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java
  4. 1 0
      config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt
  5. 55 5
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java
  6. 68 5
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java
  7. 1 0
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java
  8. 3 0
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java
  9. 46 2
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java
  10. 42 0
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java
  11. 1 0
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java
  12. 1 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java
  13. 1 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java

+ 4 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

@@ -223,6 +223,7 @@ public class OAuth2ResourceServerConfigurerTests {
 	public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception {
 		this.spring.register(WebServerConfig.class, JwkSetUriConfig.class, BasicController.class).autowire();
 		mockWebServer(jwks("Default"));
+		mockWebServer(jwks("Default"));
 		String token = this.token("ValidNoScopes");
 		// @formatter:off
 		this.mvc.perform(get("/").with(bearerToken(token)))
@@ -235,6 +236,7 @@ public class OAuth2ResourceServerConfigurerTests {
 	public void getWhenUsingJwkSetUriInLambdaThenAcceptsRequest() throws Exception {
 		this.spring.register(WebServerConfig.class, JwkSetUriInLambdaConfig.class, BasicController.class).autowire();
 		mockWebServer(jwks("Default"));
+		mockWebServer(jwks("Default"));
 		String token = this.token("ValidNoScopes");
 		// @formatter:off
 		this.mvc.perform(get("/").with(bearerToken(token)))
@@ -1185,6 +1187,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		String jwtThree = jwtFromIssuer(issuerThree);
 		mockWebServer(String.format(metadata, issuerOne, issuerOne));
 		mockWebServer(jwkSet);
+		mockWebServer(jwkSet);
 		// @formatter:off
 		this.mvc.perform(get("/authenticated").with(bearerToken(jwtOne)))
 				.andExpect(status().isOk())
@@ -1192,6 +1195,7 @@ public class OAuth2ResourceServerConfigurerTests {
 		// @formatter:on
 		mockWebServer(String.format(metadata, issuerTwo, issuerTwo));
 		mockWebServer(jwkSet);
+		mockWebServer(jwkSet);
 		// @formatter:off
 		this.mvc.perform(get("/authenticated").with(bearerToken(jwtTwo)))
 				.andExpect(status().isOk())

+ 4 - 0
config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java

@@ -148,6 +148,7 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
 	public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception {
 		this.spring.configLocations(xml("WebServer"), xml("JwkSetUri")).autowire();
 		mockWebServer(jwks("Default"));
+		mockWebServer(jwks("Default"));
 		String token = this.token("ValidNoScopes");
 		// @formatter:off
 		this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
@@ -709,18 +710,21 @@ public class OAuth2ResourceServerBeanDefinitionParserTests {
 		String jwtThree = jwtFromIssuer(issuerThree);
 		mockWebServer(String.format(metadata, issuerOne, issuerOne));
 		mockWebServer(jwkSet);
+		mockWebServer(jwkSet);
 		// @formatter:off
 		this.mvc.perform(get("/authenticated").header("Authorization", "Bearer " + jwtOne))
 				.andExpect(status().isNotFound());
 		// @formatter:on
 		mockWebServer(String.format(metadata, issuerTwo, issuerTwo));
 		mockWebServer(jwkSet);
+		mockWebServer(jwkSet);
 		// @formatter:off
 		this.mvc.perform(get("/authenticated").header("Authorization", "Bearer " + jwtTwo))
 				.andExpect(status().isNotFound());
 		// @formatter:on
 		mockWebServer(String.format(metadata, issuerThree, issuerThree));
 		mockWebServer(jwkSet);
+		mockWebServer(jwkSet);
 		// @formatter:off
 		this.mvc.perform(get("/authenticated").header("Authorization", "Bearer " + jwtThree))
 				.andExpect(status().isUnauthorized())

+ 2 - 0
config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java

@@ -261,6 +261,7 @@ public class OAuth2ResourceServerSpecTests {
 		this.spring.register(JwkSetUriConfig.class, RootController.class).autowire();
 		MockWebServer mockWebServer = this.spring.getContext().getBean(MockWebServer.class);
 		mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
+		mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
 		// @formatter:off
 		this.client.get()
 				.headers((headers) -> headers
@@ -276,6 +277,7 @@ public class OAuth2ResourceServerSpecTests {
 		this.spring.register(JwkSetUriInLambdaConfig.class, RootController.class).autowire();
 		MockWebServer mockWebServer = this.spring.getContext().getBean(MockWebServer.class);
 		mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
+		mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
 		// @formatter:off
 		this.client.get()
 				.headers((headers) -> headers

+ 1 - 0
config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt

@@ -160,6 +160,7 @@ class ServerJwtDslTests {
     fun `jwt when using custom JWK Set URI then custom URI used`() {
         this.spring.register(CustomJwkSetUriConfig::class.java).autowire()
 
+        CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet))
         CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet))
 
         this.client.get()

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

@@ -21,21 +21,26 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.security.interfaces.RSAPublicKey;
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 
 import javax.crypto.SecretKey;
 
+import com.nimbusds.jose.Algorithm;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.RemoteKeySourceException;
+import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
 import com.nimbusds.jose.jwk.source.JWKSetCache;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.jwk.source.RemoteJWKSet;
@@ -234,6 +239,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 	 */
 	public static final class JwkSetUriJwtDecoderBuilder {
 
+		private static final Log log = LogFactory.getLog(JwkSetUriJwtDecoderBuilder.class);
+
 		private String jwkSetUri;
 
 		private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
@@ -322,17 +329,60 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		}
 
 		JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
-			if (this.signatureAlgorithms.isEmpty()) {
-				return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
+			Set<SignatureAlgorithm> algorithms = new HashSet<>();
+			if (!this.signatureAlgorithms.isEmpty()) {
+				algorithms.addAll(this.signatureAlgorithms);
+			} else {
+				algorithms.addAll(fetchSignatureAlgorithms());
+			}
+
+			if (algorithms.isEmpty()) {
+				algorithms.add(SignatureAlgorithm.RS256);
 			}
+
 			Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
-			for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
-				JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
-				jwsAlgorithms.add(jwsAlgorithm);
+			for (SignatureAlgorithm signatureAlgorithm : algorithms) {
+				jwsAlgorithms.add(JWSAlgorithm.parse(signatureAlgorithm.getName()));
 			}
+
 			return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
 		}
 
+		private Set<SignatureAlgorithm> fetchSignatureAlgorithms() {
+			try {
+				return parseAlgorithms(JWKSet.load(toURL(jwkSetUri), 5000, 5000, 0));
+			} catch (Exception ex) {
+				throw new IllegalArgumentException("Failed to load Signature Algorithms from remote JWK source.", ex);
+			}
+		}
+
+		private Set<SignatureAlgorithm> parseAlgorithms(JWKSet jwkSet) {
+			if (jwkSet == null) {
+				throw new IllegalArgumentException(String.format("No JWKs received from %s", jwkSetUri));
+			}
+
+			List<JWK> jwks = new ArrayList<>();
+			for (JWK jwk : jwkSet.getKeys()) {
+				KeyUse keyUse = jwk.getKeyUse();
+				if (keyUse != null && keyUse.equals(KeyUse.SIGNATURE)) {
+					jwks.add(jwk);
+				}
+			}
+
+			Set<SignatureAlgorithm> algorithms = new HashSet<>();
+			for (JWK jwk : jwks) {
+				Algorithm algorithm = jwk.getAlgorithm();
+				if (algorithm != null) {
+					SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(algorithm.getName());
+					if (signatureAlgorithm != null) {
+						algorithms.add(signatureAlgorithm);
+					}
+				}
+			}
+
+			return algorithms;
+		}
+
 		JWKSource<SecurityContext> jwkSource(ResourceRetriever jwkSetRetriever) {
 			if (this.cache == null) {
 				return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever);

+ 68 - 5
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java

@@ -16,11 +16,14 @@
 
 package org.springframework.security.oauth2.jwt;
 
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.security.interfaces.RSAPublicKey;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -28,6 +31,7 @@ import java.util.function.Function;
 
 import javax.crypto.SecretKey;
 
+import com.nimbusds.jose.Algorithm;
 import com.nimbusds.jose.Header;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
@@ -35,6 +39,8 @@ import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jose.jwk.JWKMatcher;
 import com.nimbusds.jose.jwk.JWKSelector;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.BadJOSEException;
@@ -50,6 +56,8 @@ 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.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
@@ -273,6 +281,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	 */
 	public static final class JwkSetUriReactiveJwtDecoderBuilder {
 
+		private static final Log log = LogFactory.getLog(JwkSetUriReactiveJwtDecoderBuilder.class);
+
 		private final String jwkSetUri;
 
 		private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
@@ -354,17 +364,63 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 		}
 
 		JWSKeySelector<JWKSecurityContext> jwsKeySelector(JWKSource<JWKSecurityContext> jwkSource) {
-			if (this.signatureAlgorithms.isEmpty()) {
-				return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
+			Set<SignatureAlgorithm> algorithms = new HashSet<>();
+			if (!this.signatureAlgorithms.isEmpty()) {
+				algorithms.addAll(this.signatureAlgorithms);
+			} else {
+				algorithms.addAll(fetchSignatureAlgorithms());
+			}
+
+			if (algorithms.isEmpty()) {
+				algorithms.add(SignatureAlgorithm.RS256);
 			}
+
 			Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
-			for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
-				JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
-				jwsAlgorithms.add(jwsAlgorithm);
+			for (SignatureAlgorithm signatureAlgorithm : algorithms) {
+				jwsAlgorithms.add(JWSAlgorithm.parse(signatureAlgorithm.getName()));
 			}
+
 			return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
 		}
 
+		private Set<SignatureAlgorithm> fetchSignatureAlgorithms() {
+			if (StringUtils.isEmpty(jwkSetUri)) {
+				return Collections.emptySet();
+			}
+			try {
+				return parseAlgorithms(JWKSet.load(toURL(jwkSetUri), 5000, 5000, 0));
+			} catch (Exception ex) {
+				throw new IllegalArgumentException("Failed to load Signature Algorithms from remote JWK source.", ex);
+			}
+		}
+
+		private Set<SignatureAlgorithm> parseAlgorithms(JWKSet jwkSet) {
+			if (jwkSet == null) {
+				throw new IllegalArgumentException(String.format("No JWKs received from %s", jwkSetUri));
+			}
+
+			List<JWK> jwks = new ArrayList<>();
+			for (JWK jwk : jwkSet.getKeys()) {
+				KeyUse keyUse = jwk.getKeyUse();
+				if (keyUse != null && keyUse.equals(KeyUse.SIGNATURE)) {
+					jwks.add(jwk);
+				}
+			}
+
+			Set<SignatureAlgorithm> algorithms = new HashSet<>();
+			for (JWK jwk : jwks) {
+				Algorithm algorithm = jwk.getAlgorithm();
+				if (algorithm != null) {
+					SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(algorithm.getName());
+					if (signatureAlgorithm != null) {
+						algorithms.add(signatureAlgorithm);
+					}
+				}
+			}
+
+			return algorithms;
+		}
+
 		Converter<JWT, Mono<JWTClaimsSet>> processor() {
 			JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet();
 			DefaultJWTProcessor<JWKSecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
@@ -399,6 +455,13 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 			return new JWKSelector(JWKMatcher.forJWSHeader(jwsHeader));
 		}
 
+		private static URL toURL(String url) {
+			try {
+				return new URL(url);
+			} catch (MalformedURLException ex) {
+				throw new IllegalArgumentException("Invalid JWK Set URL \"" + url + "\" : " + ex.getMessage(), ex);
+			}
+		}
 	}
 
 	/**

+ 1 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java

@@ -305,6 +305,7 @@ public class JwtDecodersTests {
 	private void prepareConfigurationResponse(String body) {
 		this.server.enqueue(response(body));
 		this.server.enqueue(response(JWK_SET));
+		this.server.enqueue(response(JWK_SET));
 	}
 
 	private void prepareConfigurationResponseOidc() {

+ 3 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java

@@ -136,6 +136,7 @@ public class NimbusJwtDecoderJwkSupportTests {
 	@Test
 	public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() throws Exception {
 		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET));
 			server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET));
 			String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
 			NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl);
@@ -151,6 +152,7 @@ public class NimbusJwtDecoderJwkSupportTests {
 	@Test
 	public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsJwtException() throws Exception {
 		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET));
 			server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET));
 			String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
 			NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl);
@@ -167,6 +169,7 @@ public class NimbusJwtDecoderJwkSupportTests {
 	@Test
 	public void decodeWhenCustomRestOperationsSetThenUsed() throws Exception {
 		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody(JWK_SET));
 			server.enqueue(new MockResponse().setBody(JWK_SET));
 			String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
 			NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl);

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

@@ -53,6 +53,7 @@ import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.jwt.proc.BadJWTException;
 import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import com.nimbusds.jwt.proc.JWTProcessor;
+import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -97,6 +98,33 @@ public class NimbusJwtDecoderTests {
 
 	private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
 
+	private static final String JWK_SET_MULTIPLE = "{\n" +
+			"  \"keys\": [\n" +
+			"    {\n" +
+			"      \"kty\": \"EC\",\n" +
+			"      \"use\": \"sig\",\n" +
+			"      \"crv\": \"P-256\",\n" +
+			"      \"x\": \"9w9ddaCKCdOfyKsENWI_cf90XmWRDISBrWf2vNo-TpE\",\n" +
+			"      \"y\": \"CThkQsCBR6dC-Y8-MVf6NFTYvMiJtjBx1x0Pbr-kP5c\",\n" +
+			"      \"alg\": \"ES256\"\n" +
+			"    },\n" +
+			"    {\n" +
+			"      \"kty\": \"RSA\",\n" +
+			"      \"e\": \"AQAB\",\n" +
+			"      \"use\": \"sig\",\n" +
+			"      \"alg\": \"RS256\",\n" +
+			"      \"n\": \"rNXfHmPwwPcmyjIG0gfBdera44Y6C6jhqgGAxCFlxrhveOAy12ff3Z0oyu0fsB-q2eVQ1amBYUWaNCopVuZEBx9GcNs0KmkAmh0bQVAT9rI81CE6thuZiNfnNaqcIHnvUa__1wnR1PzX7mDyvcVtxSC6VbQo9jt6ouBXaW6ZolqzlfbDAU-2FJpE2YLoqMs1PtSss_gYiXrP0f9GLomcQTWgsw-VNc9iYJZG5K8kIKlo_bu6YQf7GoGt4IEUd-dQBpavIBL7jjRKp30zY94J4QAwPo_UnO_EpDuUa9QyO6kuk6A3yv0nfstK-4wE1Jr42tlDO1SFzRzy_aYAjT7Ozw\"\n" +
+			"    },\n" +
+			"    {\n" +
+			"      \"kty\": \"EC\",\n" +
+			"      \"use\": \"sig\",\n" +
+			"      \"crv\": \"P-384\",\n" +
+			"      \"x\": \"71M1BlzONOc9LYuOB-xmK8Y3njqqGTJLguDLd7geILqYDiWrH5ELb9SKtVYcQvD1\",\n" +
+			"      \"y\": \"Lv8lK0ukUNFa1Vhlzbi8VDdIfHrd2IEmUp21fmLNwPwTMJLbDGYoPm4DgYfzOfSm\"\n" +
+			"    }\n" +
+			"  ]\n" +
+			"}";
+
 	private static final String MALFORMED_JWK_SET = "malformed";
 
 	private static final String SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA";
@@ -279,8 +307,8 @@ public class NimbusJwtDecoderTests {
 	public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsJwtException() throws Exception {
 		try (MockWebServer server = new MockWebServer()) {
 			String jwkSetUri = server.url("/.well-known/jwks.json").toString();
-			NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
 			server.shutdown();
+			NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
 			// @formatter:off
 			assertThatExceptionOfType(JwtException.class)
 					.isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT))
@@ -295,8 +323,8 @@ public class NimbusJwtDecoderTests {
 		try (MockWebServer server = new MockWebServer()) {
 			Cache cache = new ConcurrentMapCache("test-jwk-set-cache");
 			String jwkSetUri = server.url("/.well-known/jwks.json").toString();
-			NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).cache(cache).build();
 			server.shutdown();
+			NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).cache(cache).build();
 			// @formatter:off
 			assertThatExceptionOfType(JwtException.class)
 					.isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT))
@@ -604,6 +632,22 @@ public class NimbusJwtDecoderTests {
 		assertThat(jwsAlgorithmMapKeySelector.isAllowed(JWSAlgorithm.RS512)).isTrue();
 	}
 
+	@Test
+	public void jwsKeySetWithMultipleJWKThenMultipleAlgorithmsInSelector() throws Exception {
+		try ( MockWebServer server = new MockWebServer() ) {
+			Cache cache = new ConcurrentMapCache("test-jwk-set-cache");
+			server.enqueue(new MockResponse().setBody(JWK_SET_MULTIPLE));
+			String jwkSetUri = server.url("/.well-known/jwks.json").toString();
+			NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri);
+			builder.cache(cache);
+			DefaultJWTProcessor<SecurityContext> processor = (DefaultJWTProcessor<SecurityContext>) builder.processor();
+			JWSVerificationKeySelector<SecurityContext> selector = (JWSVerificationKeySelector<SecurityContext>) processor.getJWSKeySelector();
+			server.shutdown();
+			assertThat(selector.isAllowed(JWSAlgorithm.RS256)).isTrue();
+			assertThat(selector.isAllowed(JWSAlgorithm.ES256)).isTrue();
+		}
+	}
+
 	// gh-7290
 	@Test
 	public void decodeWhenJwkSetRequestedThenAcceptHeaderJsonAndJwkSetJson() {

+ 42 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java

@@ -38,6 +38,7 @@ import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jose.JWSSigner;
 import com.nimbusds.jose.crypto.MACSigner;
 import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
 import com.nimbusds.jose.proc.JWKSecurityContext;
@@ -100,6 +101,33 @@ public class NimbusReactiveJwtDecoderTests {
 			+ "}";
 	// @formatter:on
 
+	private static final String JWK_SET_MULTIPLE = "{\n" +
+			"  \"keys\": [\n" +
+			"    {\n" +
+			"      \"kty\": \"EC\",\n" +
+			"      \"use\": \"sig\",\n" +
+			"      \"crv\": \"P-256\",\n" +
+			"      \"x\": \"9w9ddaCKCdOfyKsENWI_cf90XmWRDISBrWf2vNo-TpE\",\n" +
+			"      \"y\": \"CThkQsCBR6dC-Y8-MVf6NFTYvMiJtjBx1x0Pbr-kP5c\",\n" +
+			"      \"alg\": \"ES256\"\n" +
+			"    },\n" +
+			"    {\n" +
+			"      \"kty\": \"RSA\",\n" +
+			"      \"e\": \"AQAB\",\n" +
+			"      \"use\": \"sig\",\n" +
+			"      \"alg\": \"RS256\",\n" +
+			"      \"n\": \"rNXfHmPwwPcmyjIG0gfBdera44Y6C6jhqgGAxCFlxrhveOAy12ff3Z0oyu0fsB-q2eVQ1amBYUWaNCopVuZEBx9GcNs0KmkAmh0bQVAT9rI81CE6thuZiNfnNaqcIHnvUa__1wnR1PzX7mDyvcVtxSC6VbQo9jt6ouBXaW6ZolqzlfbDAU-2FJpE2YLoqMs1PtSss_gYiXrP0f9GLomcQTWgsw-VNc9iYJZG5K8kIKlo_bu6YQf7GoGt4IEUd-dQBpavIBL7jjRKp30zY94J4QAwPo_UnO_EpDuUa9QyO6kuk6A3yv0nfstK-4wE1Jr42tlDO1SFzRzy_aYAjT7Ozw\"\n" +
+			"    },\n" +
+			"    {\n" +
+			"      \"kty\": \"EC\",\n" +
+			"      \"use\": \"sig\",\n" +
+			"      \"crv\": \"P-384\",\n" +
+			"      \"x\": \"71M1BlzONOc9LYuOB-xmK8Y3njqqGTJLguDLd7geILqYDiWrH5ELb9SKtVYcQvD1\",\n" +
+			"      \"y\": \"Lv8lK0ukUNFa1Vhlzbi8VDdIfHrd2IEmUp21fmLNwPwTMJLbDGYoPm4DgYfzOfSm\"\n" +
+			"    }\n" +
+			"  ]\n" +
+			"}";
+
 	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";
@@ -124,6 +152,7 @@ public class NimbusReactiveJwtDecoderTests {
 		this.server = new MockWebServer();
 		this.server.start();
 		this.server.enqueue(new MockResponse().setBody(this.jwkSet));
+		this.server.enqueue(new MockResponse().setBody(this.jwkSet));
 		this.decoder = new NimbusReactiveJwtDecoder(this.server.url("/certs").toString());
 	}
 
@@ -589,6 +618,19 @@ public class NimbusReactiveJwtDecoderTests {
 		assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)).isTrue();
 	}
 
+	@Test
+	public void jwsKeySetWithMultipleJWKThenMultipleAlgorithmsInSelector() throws Exception {
+		try (MockWebServer server = new MockWebServer()) {
+			server.enqueue(new MockResponse().setBody(JWK_SET_MULTIPLE));
+			String jwkSetUri = server.url("/.well-known/jwks.json").toString();
+			NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri);
+			JWSVerificationKeySelector<JWKSecurityContext> selector = (JWSVerificationKeySelector<JWKSecurityContext>) builder.jwsKeySelector(new JWKSecurityContextJWKSet());
+			server.shutdown();
+			assertThat(selector.isAllowed(JWSAlgorithm.RS256)).isTrue();
+			assertThat(selector.isAllowed(JWSAlgorithm.ES256)).isTrue();
+		}
+	}
+
 	@Test
 	public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() {
 		JWKSource<JWKSecurityContext> jwkSource = mock(JWKSource.class);

+ 1 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java

@@ -282,6 +282,7 @@ public class ReactiveJwtDecodersTests {
 	private void prepareConfigurationResponse(String body) {
 		this.server.enqueue(response(body));
 		this.server.enqueue(response(JWK_SET));
+		this.server.enqueue(response(JWK_SET));
 	}
 
 	private void prepareConfigurationResponseOidc() {

+ 1 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java

@@ -70,6 +70,7 @@ public class JwtIssuerAuthenticationManagerResolverTests {
 					.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)
 			));
 			// @formatter:on
+			server.enqueue(new MockResponse().setResponseCode(200));
 			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));

+ 1 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java

@@ -71,6 +71,7 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests {
 			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));
 			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));