diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 0057529010..6dbcf62e73 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/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()) diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java index 2cfdfc536c..6c93819f30 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java +++ b/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()) diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index 09c8b4ae45..ff2d574425 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/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 diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt index ddb33b5323..0d03cc85e8 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt +++ b/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() diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index c4e80d2316..478e220724 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/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 signatureAlgorithms = new HashSet<>(); @@ -322,17 +329,60 @@ public final class NimbusJwtDecoder implements JwtDecoder { } JWSKeySelector jwsKeySelector(JWKSource jwkSource) { - if (this.signatureAlgorithms.isEmpty()) { - return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); + Set algorithms = new HashSet<>(); + if (!this.signatureAlgorithms.isEmpty()) { + algorithms.addAll(this.signatureAlgorithms); + } else { + algorithms.addAll(fetchSignatureAlgorithms()); + } + + if (algorithms.isEmpty()) { + algorithms.add(SignatureAlgorithm.RS256); } + Set 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 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 parseAlgorithms(JWKSet jwkSet) { + if (jwkSet == null) { + throw new IllegalArgumentException(String.format("No JWKs received from %s", jwkSetUri)); + } + + List jwks = new ArrayList<>(); + for (JWK jwk : jwkSet.getKeys()) { + KeyUse keyUse = jwk.getKeyUse(); + if (keyUse != null && keyUse.equals(KeyUse.SIGNATURE)) { + jwks.add(jwk); + } + } + + Set 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 jwkSource(ResourceRetriever jwkSetRetriever) { if (this.cache == null) { return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever); 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 122cf14c37..20e61460f1 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 @@ -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 signatureAlgorithms = new HashSet<>(); @@ -354,17 +364,63 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { } JWSKeySelector jwsKeySelector(JWKSource jwkSource) { - if (this.signatureAlgorithms.isEmpty()) { - return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); + Set algorithms = new HashSet<>(); + if (!this.signatureAlgorithms.isEmpty()) { + algorithms.addAll(this.signatureAlgorithms); + } else { + algorithms.addAll(fetchSignatureAlgorithms()); + } + + if (algorithms.isEmpty()) { + algorithms.add(SignatureAlgorithm.RS256); } + Set 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 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 parseAlgorithms(JWKSet jwkSet) { + if (jwkSet == null) { + throw new IllegalArgumentException(String.format("No JWKs received from %s", jwkSetUri)); + } + + List jwks = new ArrayList<>(); + for (JWK jwk : jwkSet.getKeys()) { + KeyUse keyUse = jwk.getKeyUse(); + if (keyUse != null && keyUse.equals(KeyUse.SIGNATURE)) { + jwks.add(jwk); + } + } + + Set 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> processor() { JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet(); DefaultJWTProcessor 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); + } + } } /** diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java index 868fe4713b..18e993c13c 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java +++ b/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() { diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java index 28f407ded2..faeaa6ea43 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupportTests.java +++ b/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); diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java index 3181054742..8e6f035a7e 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java +++ b/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 processor = (DefaultJWTProcessor) builder.processor(); + JWSVerificationKeySelector selector = (JWSVerificationKeySelector) processor.getJWSKeySelector(); + server.shutdown(); + assertThat(selector.isAllowed(JWSAlgorithm.RS256)).isTrue(); + assertThat(selector.isAllowed(JWSAlgorithm.ES256)).isTrue(); + } + } + // gh-7290 @Test public void decodeWhenJwkSetRequestedThenAcceptHeaderJsonAndJwkSetJson() { 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 bdbf96e66e..8a76667520 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 @@ -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 selector = (JWSVerificationKeySelector) builder.jwsKeySelector(new JWKSecurityContextJWKSet()); + server.shutdown(); + assertThat(selector.isAllowed(JWSAlgorithm.RS256)).isTrue(); + assertThat(selector.isAllowed(JWSAlgorithm.ES256)).isTrue(); + } + } + @Test public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() { JWKSource jwkSource = mock(JWKSource.class); diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java index 6c5d22ef30..ce7ca49927 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java +++ b/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() { 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 354c551105..993a06669d 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 @@ -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)); 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 a794aa1fde..db338597eb 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 @@ -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));