From 366146ff8077dc83441e5a0b4e1f42ba3c02f08e Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 18 Sep 2020 15:53:46 -0600 Subject: [PATCH] Polish JWT Signature Algorithm Discovery - Moved support to JwtDecoders and ReactiveJwtDecoders since there is already the expectation that those classes make an outbound connection to complete configuration. Since there's no outbound connection when configuring a NimbusJwtDecoder or NimbusReactiveJwtDecoder, it would be more intrusive to change that. Closes gh-7160 --- .../OAuth2ResourceServerConfigurerTests.java | 3 +- ...sourceServerBeanDefinitionParserTests.java | 1 - .../server/OAuth2ResourceServerSpecTests.java | 2 - .../config/web/server/ServerJwtDslTests.kt | 1 - .../servlet/oauth2/oauth2-resourceserver.adoc | 7 +- .../JwtDecoderProviderConfigurationUtils.java | 48 ++++++++++++ .../security/oauth2/jwt/JwtDecoders.java | 24 +++++- .../security/oauth2/jwt/NimbusJwtDecoder.java | 60 ++------------- .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 73 ++---------------- .../oauth2/jwt/ReactiveJwtDecoders.java | 25 +++++- ...ecoderProviderConfigurationUtilsTests.java | 76 +++++++++++++++++++ .../security/oauth2/jwt/JwtDecodersTests.java | 2 +- .../jwt/NimbusJwtDecoderJwkSupportTests.java | 3 - .../oauth2/jwt/NimbusJwtDecoderTests.java | 48 +----------- .../jwt/NimbusReactiveJwtDecoderTests.java | 42 ---------- .../oauth2/jwt/ReactiveJwtDecodersTests.java | 2 +- ...uerAuthenticationManagerResolverTests.java | 7 +- ...iveAuthenticationManagerResolverTests.java | 5 +- 18 files changed, 199 insertions(+), 230 deletions(-) create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java 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 6dbcf62e73..05fad9f470 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,7 +223,6 @@ 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))) @@ -236,7 +235,6 @@ 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))) @@ -1203,6 +1201,7 @@ public class OAuth2ResourceServerConfigurerTests { // @formatter:on mockWebServer(String.format(metadata, issuerThree, issuerThree)); mockWebServer(jwkSet); + mockWebServer(jwkSet); // @formatter:off this.mvc.perform(get("/authenticated").with(bearerToken(jwtThree))) .andExpect(status().isUnauthorized()) 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 6c93819f30..498d8f80be 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,7 +148,6 @@ 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)) 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 ff2d574425..09c8b4ae45 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,7 +261,6 @@ 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 @@ -277,7 +276,6 @@ 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 0d03cc85e8..ddb33b5323 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,7 +160,6 @@ 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/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc index 3db56df0c6..b4e520c767 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc @@ -99,9 +99,10 @@ When this property and these dependencies are used, Resource Server will automat It achieves this through a deterministic startup process: -1. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property -2. Configure the validation strategy to query `jwks_url` for valid public keys -3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`. +1. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property +2. Query the `jwks_url` endpoint for supported algorithms +3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found +4. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`. A consequence of this process is that the authorization server must be up and receiving requests in order for Resource Server to successfully start up. diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java index 5cf7ace331..ebe3c60ada 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java @@ -18,11 +18,25 @@ package org.springframework.security.oauth2.jwt; import java.net.URI; import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.util.Assert; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; @@ -68,6 +82,40 @@ final class JwtDecoderProviderConfigurationUtils { + "\" provided in the configuration did not " + "match the requested issuer \"" + issuer + "\""); } + static Set getSignatureAlgorithms(JWKSource jwkSource) { + JWKMatcher jwkMatcher = new JWKMatcher.Builder().publicOnly(true).keyUses(KeyUse.SIGNATURE, null) + .keyTypes(KeyType.RSA, KeyType.EC).build(); + Set jwsAlgorithms = new HashSet<>(); + try { + List jwks = jwkSource.get(new JWKSelector(jwkMatcher), null); + for (JWK jwk : jwks) { + if (jwk.getAlgorithm() != null) { + jwsAlgorithms.add((JWSAlgorithm) jwk.getAlgorithm()); + } + else { + if (jwk.getKeyType() == KeyType.RSA) { + jwsAlgorithms.addAll(JWSAlgorithm.Family.RSA); + } + else if (jwk.getKeyType() == KeyType.EC) { + jwsAlgorithms.addAll(JWSAlgorithm.Family.EC); + } + } + } + } + catch (KeySourceException ex) { + throw new IllegalStateException(ex); + } + Set signatureAlgorithms = new HashSet<>(); + for (JWSAlgorithm jwsAlgorithm : jwsAlgorithms) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(jwsAlgorithm.getName()); + if (signatureAlgorithm != null) { + signatureAlgorithms.add(signatureAlgorithm); + } + } + Assert.notEmpty(signatureAlgorithms, "Failed to find any algorithms from the JWK set"); + return signatureAlgorithms; + } + private static String getMetadataIssuer(Map configuration) { if (configuration.containsKey("issuer")) { return configuration.get("issuer").toString(); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java index d109cc0d2d..7bd183dbaa 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java @@ -16,9 +16,17 @@ package org.springframework.security.oauth2.jwt; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; import java.util.Map; +import java.util.Set; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.SecurityContext; import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.util.Assert; /** @@ -106,9 +114,23 @@ public final class JwtDecoders { private static JwtDecoder withProviderConfiguration(Map configuration, String issuer) { JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); OAuth2TokenValidator jwtValidator = JwtValidators.createDefaultWithIssuer(issuer); - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(configuration.get("jwks_uri").toString()).build(); + String jwkSetUri = configuration.get("jwks_uri").toString(); + RemoteJWKSet jwkSource = new RemoteJWKSet<>(url(jwkSetUri)); + Set signatureAlgorithms = JwtDecoderProviderConfigurationUtils + .getSignatureAlgorithms(jwkSource); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri) + .jwsAlgorithms((algs) -> algs.addAll(signatureAlgorithms)).build(); jwtDecoder.setJwtValidator(jwtValidator); return jwtDecoder; } + private static URL url(String url) { + try { + return new URL(url); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } 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 478e220724..c4e80d2316 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,26 +21,21 @@ 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; @@ -239,8 +234,6 @@ 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<>(); @@ -329,60 +322,17 @@ public final class NimbusJwtDecoder implements JwtDecoder { } JWSKeySelector jwsKeySelector(JWKSource jwkSource) { - Set algorithms = new HashSet<>(); - if (!this.signatureAlgorithms.isEmpty()) { - algorithms.addAll(this.signatureAlgorithms); - } else { - algorithms.addAll(fetchSignatureAlgorithms()); - } - - if (algorithms.isEmpty()) { - algorithms.add(SignatureAlgorithm.RS256); + if (this.signatureAlgorithms.isEmpty()) { + return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); } - Set jwsAlgorithms = new HashSet<>(); - for (SignatureAlgorithm signatureAlgorithm : algorithms) { - jwsAlgorithms.add(JWSAlgorithm.parse(signatureAlgorithm.getName())); + for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); + jwsAlgorithms.add(jwsAlgorithm); } - 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 20e61460f1..122cf14c37 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,14 +16,11 @@ 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; @@ -31,7 +28,6 @@ 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; @@ -39,8 +35,6 @@ 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; @@ -56,8 +50,6 @@ 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; @@ -281,8 +273,6 @@ 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<>(); @@ -364,63 +354,17 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { } JWSKeySelector jwsKeySelector(JWKSource jwkSource) { - Set algorithms = new HashSet<>(); - if (!this.signatureAlgorithms.isEmpty()) { - algorithms.addAll(this.signatureAlgorithms); - } else { - algorithms.addAll(fetchSignatureAlgorithms()); - } - - if (algorithms.isEmpty()) { - algorithms.add(SignatureAlgorithm.RS256); + if (this.signatureAlgorithms.isEmpty()) { + return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); } - Set jwsAlgorithms = new HashSet<>(); - for (SignatureAlgorithm signatureAlgorithm : algorithms) { - jwsAlgorithms.add(JWSAlgorithm.parse(signatureAlgorithm.getName())); + for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); + jwsAlgorithms.add(jwsAlgorithm); } - 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<>(); @@ -455,13 +399,6 @@ 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/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java index c279cc5819..aa59b43737 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java @@ -16,9 +16,17 @@ package org.springframework.security.oauth2.jwt; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; import java.util.Map; +import java.util.Set; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.SecurityContext; import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.util.Assert; /** @@ -106,10 +114,23 @@ public final class ReactiveJwtDecoders { private static ReactiveJwtDecoder withProviderConfiguration(Map configuration, String issuer) { JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); OAuth2TokenValidator jwtValidator = JwtValidators.createDefaultWithIssuer(issuer); - NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder - .withJwkSetUri(configuration.get("jwks_uri").toString()).build(); + String jwkSetUri = configuration.get("jwks_uri").toString(); + RemoteJWKSet jwkSource = new RemoteJWKSet<>(url(jwkSetUri)); + Set signatureAlgorithms = JwtDecoderProviderConfigurationUtils + .getSignatureAlgorithms(jwkSource); + NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri) + .jwsAlgorithms((algs) -> algs.addAll(signatureAlgorithms)).build(); jwtDecoder.setJwtValidator(jwtValidator); return jwtDecoder; } + private static URL url(String url) { + try { + return new URL(url); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java new file mode 100644 index 0000000000..31de6e4c23 --- /dev/null +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtilsTests.java @@ -0,0 +1,76 @@ +/* + * 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.jwt; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.util.Base64URL; +import org.junit.Test; + +import org.springframework.security.oauth2.jose.TestKeys; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.when; + +public class JwtDecoderProviderConfigurationUtilsTests { + + @Test + public void getSignatureAlgorithmsWhenJwkSetSpecifiesAlgorithmThenUses() throws Exception { + JWKSource jwkSource = mock(JWKSource.class); + RSAKey key = new RSAKey.Builder(TestKeys.DEFAULT_PUBLIC_KEY).keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.RS384).build(); + when(jwkSource.get(any(JWKSelector.class), isNull())).thenReturn(Collections.singletonList(key)); + Set algorithms = JwtDecoderProviderConfigurationUtils.getSignatureAlgorithms(jwkSource); + assertThat(algorithms).containsOnly(SignatureAlgorithm.RS384); + } + + @Test + public void getSignatureAlgorithmsWhenJwkSetIsEmptyThenIllegalArgumentException() throws Exception { + JWKSource jwkSource = mock(JWKSource.class); + when(jwkSource.get(any(JWKSelector.class), isNull())).thenReturn(Collections.emptyList()); + assertThatIllegalArgumentException() + .isThrownBy(() -> JwtDecoderProviderConfigurationUtils.getSignatureAlgorithms(jwkSource)); + } + + @Test + public void getSignatureAlgorithmsWhenJwkSetSpecifiesFamilyThenUses() throws Exception { + JWKSource jwkSource = mock(JWKSource.class); + // Test parameters are from Anders Rundgren, public only + ECKey ecKey = new ECKey.Builder(Curve.P_256, new Base64URL("3l2Da_flYc-AuUTm2QzxgyvJxYM_2TeB9DMlwz7j1PE"), + new Base64URL("-kjT7Wrfhwsi9SG6H4UXiyUiVE9GHCLauslksZ3-_t0")).keyUse(KeyUse.SIGNATURE).build(); + RSAKey rsaKey = new RSAKey.Builder(TestKeys.DEFAULT_PUBLIC_KEY).keyUse(KeyUse.ENCRYPTION).build(); + when(jwkSource.get(any(JWKSelector.class), isNull())).thenReturn(Arrays.asList(ecKey, rsaKey)); + Set algorithms = JwtDecoderProviderConfigurationUtils.getSignatureAlgorithms(jwkSource); + assertThat(algorithms).contains(SignatureAlgorithm.ES256, SignatureAlgorithm.ES384, SignatureAlgorithm.ES512); + } + +} 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 18e993c13c..92a1966699 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 @@ -80,7 +80,7 @@ public class JwtDecodersTests { + "}"; // @formatter:on - 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 = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; private static final String ISSUER_MISMATCH = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvd3Jvbmdpc3N1ZXIiLCJleHAiOjQ2ODcyNTYwNDl9.Ax8LMI6rhB9Pv_CE3kFi1JPuLj9gZycifWrLeDpkObWEEVAsIls9zAhNFyJlG-Oo7up6_mDhZgeRfyKnpSF5GhKJtXJDCzwg0ZDVUE6rS0QadSxsMMGbl7c4y0lG_7TfLX2iWeNJukJj_oSW9KzW4FsBp1BoocWjrreesqQU3fZHbikH-c_Fs2TsAIpHnxflyEzfOFWpJ8D4DtzHXqfvieMwpy42xsPZK3LR84zlasf0Ne1tC_hLHvyHRdAXwn0CMoKxc7-8j0r9Mq8kAzUsPn9If7bMLqGkxUcTPdk5x7opAUajDZx95SXHLmtztNtBa2S6EfPJXuPKG6tM5Wq5Ug"; 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 faeaa6ea43..28f407ded2 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,7 +136,6 @@ 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); @@ -152,7 +151,6 @@ 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); @@ -169,7 +167,6 @@ 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 8e6f035a7e..3181054742 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,7 +53,6 @@ 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; @@ -98,33 +97,6 @@ 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"; @@ -307,8 +279,8 @@ public class NimbusJwtDecoderTests { public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsJwtException() throws Exception { try (MockWebServer server = new MockWebServer()) { String jwkSetUri = server.url("/.well-known/jwks.json").toString(); - server.shutdown(); NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); + server.shutdown(); // @formatter:off assertThatExceptionOfType(JwtException.class) .isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT)) @@ -323,8 +295,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(); - server.shutdown(); NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).cache(cache).build(); + server.shutdown(); // @formatter:off assertThatExceptionOfType(JwtException.class) .isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT)) @@ -632,22 +604,6 @@ 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 8a76667520..bdbf96e66e 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,7 +38,6 @@ 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; @@ -101,33 +100,6 @@ 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"; @@ -152,7 +124,6 @@ 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()); } @@ -618,19 +589,6 @@ 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 ce7ca49927..9503faba02 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 @@ -79,7 +79,7 @@ public class ReactiveJwtDecodersTests { + "}"; // @formatter:on - 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 = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; private static final String ISSUER_MISMATCH = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczpcL1wvd3Jvbmdpc3N1ZXIiLCJleHAiOjQ2ODcyNTYwNDl9.Ax8LMI6rhB9Pv_CE3kFi1JPuLj9gZycifWrLeDpkObWEEVAsIls9zAhNFyJlG-Oo7up6_mDhZgeRfyKnpSF5GhKJtXJDCzwg0ZDVUE6rS0QadSxsMMGbl7c4y0lG_7TfLX2iWeNJukJj_oSW9KzW4FsBp1BoocWjrreesqQU3fZHbikH-c_Fs2TsAIpHnxflyEzfOFWpJ8D4DtzHXqfvieMwpy42xsPZK3LR84zlasf0Ne1tC_hLHvyHRdAXwn0CMoKxc7-8j0r9Mq8kAzUsPn9If7bMLqGkxUcTPdk5x7opAUajDZx95SXHLmtztNtBa2S6EfPJXuPKG6tM5Wq5Ug"; 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 993a06669d..c6d3a5397a 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 @@ -53,6 +53,8 @@ public class JwtIssuerAuthenticationManagerResolverTests { 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\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; + private String jwt = jwt("iss", "trusted"); private String evil = jwt("iss", "\""); @@ -69,8 +71,11 @@ public class JwtIssuerAuthenticationManagerResolverTests { .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 - 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 db338597eb..02979d65bc 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 @@ -59,6 +59,8 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { + "}"; // @formatter:on + private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; + private String jwt = jwt("iss", "trusted"); private String evil = jwt("iss", "\""); @@ -71,7 +73,8 @@ 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)); + 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));