diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java index e876d1433b57..7c1c93756621 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java @@ -20,9 +20,11 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.core.io.Resource; import org.springframework.util.Assert; @@ -59,9 +61,9 @@ public static class Jwt { private String jwkSetUri; /** - * JSON Web Algorithm used for verifying the digital signatures. + * JSON Web Algorithms used for verifying the digital signatures. */ - private String jwsAlgorithm = "RS256"; + private List jwsAlgorithms = Arrays.asList("RS256"); /** * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 @@ -87,12 +89,23 @@ public void setJwkSetUri(String jwkSetUri) { this.jwkSetUri = jwkSetUri; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.security.oauth2.resourceserver.jwt.jws-algorithms") public String getJwsAlgorithm() { - return this.jwsAlgorithm; + return this.jwsAlgorithms.isEmpty() ? null : this.jwsAlgorithms.get(0); } + @Deprecated public void setJwsAlgorithm(String jwsAlgorithm) { - this.jwsAlgorithm = jwsAlgorithm; + this.jwsAlgorithms = new ArrayList<>(Arrays.asList(jwsAlgorithm)); + } + + public List getJwsAlgorithms() { + return this.jwsAlgorithms; + } + + public void setJwsAlgorithms(List jwsAlgortithms) { + this.jwsAlgorithms = jwsAlgortithms; } public String getIssuerUri() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 2d770a7fe8f2..715aa69dd207 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -23,6 +23,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Supplier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -78,8 +79,7 @@ static class JwtConfiguration { @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") ReactiveJwtDecoder jwtDecoder() { NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder - .withJwkSetUri(this.properties.getJwkSetUri()) - .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + .withJwkSetUri(this.properties.getJwkSetUri()).jwsAlgorithms(this::jwsAlgorithms).build(); String issuerUri = this.properties.getIssuerUri(); Supplier> defaultValidator = (issuerUri != null) ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault; @@ -87,6 +87,12 @@ ReactiveJwtDecoder jwtDecoder() { return nimbusReactiveJwtDecoder; } + private void jwsAlgorithms(Set signatureAlgorithms) { + for (String algorithm : this.properties.getJwsAlgorithms()) { + signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); + } + } + private OAuth2TokenValidator getValidators(Supplier> defaultValidator) { OAuth2TokenValidator defaultValidators = defaultValidator.get(); List audiences = this.properties.getAudiences(); @@ -106,7 +112,7 @@ NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey) - .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())).build(); jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault)); return jwtDecoder; } @@ -116,6 +122,17 @@ private byte[] getKeySpec(String keyValue) { return Base64.getMimeDecoder().decode(keyValue); } + private String exactlyOneAlgorithm() { + List algorithms = this.properties.getJwsAlgorithms(); + int count = (algorithms != null) ? algorithms.size() : 0; + if (count != 1) { + throw new IllegalStateException( + "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count + + " were configured"); + } + return algorithms.get(0); + } + @Bean @Conditional(IssuerUriCondition.class) SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java index 14d37c2bee77..479ace9b4321 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -23,6 +23,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Supplier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -78,7 +79,7 @@ static class JwtDecoderConfiguration { @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") JwtDecoder jwtDecoderByJwkKeySetUri() { NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) - .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + .jwsAlgorithms(this::jwsAlgorithms).build(); String issuerUri = this.properties.getIssuerUri(); Supplier> defaultValidator = (issuerUri != null) ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault; @@ -86,6 +87,12 @@ JwtDecoder jwtDecoderByJwkKeySetUri() { return nimbusJwtDecoder; } + private void jwsAlgorithms(Set signatureAlgorithms) { + for (String algorithm : this.properties.getJwsAlgorithms()) { + signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); + } + } + private OAuth2TokenValidator getValidators(Supplier> defaultValidator) { OAuth2TokenValidator defaultValidators = defaultValidator.get(); List audiences = this.properties.getAudiences(); @@ -105,7 +112,7 @@ JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey) - .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())).build(); jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault)); return jwtDecoder; } @@ -115,6 +122,17 @@ private byte[] getKeySpec(String keyValue) { return Base64.getMimeDecoder().decode(keyValue); } + private String exactlyOneAlgorithm() { + List algorithms = this.properties.getJwsAlgorithms(); + int count = (algorithms != null) ? algorithms.size() : 0; + if (count != 1) { + throw new IllegalStateException( + "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count + + " were configured"); + } + return algorithms.get(0); + } + @Bean @Conditional(IssuerUriCondition.class) SupplierJwtDecoder jwtDecoderByIssuerUri() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 15636a04d9bd..7003a59412ba 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2058,6 +2058,11 @@ "name": "spring.security.filter.order", "defaultValue": -100 }, + { + "name": "spring.security.oauth2.resourceserver.jwt.jws-algorithm", + "description": "JSON Web Algorithm used for verifying the digital signatures.", + "defaultValue": "RS256" + }, { "name": "spring.session.hazelcast.flush-mode", "defaultValue": "on-save" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index e0f51ce2ee13..cb339cbd7ddf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -32,6 +32,7 @@ import com.nimbusds.jose.JWSAlgorithm; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -114,6 +115,7 @@ void autoConfigurationShouldConfigureResourceServer() { @SuppressWarnings("unchecked") @Test + @Deprecated void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingJwsAlgorithm() { this.contextRunner .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", @@ -126,6 +128,33 @@ void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingJwsAlgorit } @Test + void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingSingleJwsAlgorithm() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS512") + .run((context) -> { + NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class); + assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$2.arg$1.jwsAlgs") + .asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class)) + .containsExactlyInAnyOrder(JWSAlgorithm.RS512); + }); + } + + @Test + void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingMultipleJwsAlgorithms() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS256, RS384, RS512") + .run((context) -> { + NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class); + assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$2.arg$1.jwsAlgs") + .asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class)) + .containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512); + }); + } + + @Test + @Deprecated void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingJwsAlgorithm() { this.contextRunner.withPropertyValues( "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", @@ -136,6 +165,29 @@ void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingJwsAl }); } + @Test + void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingSingleJwsAlgorithm() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384").run((context) -> { + NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class); + assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$1.jwsKeySelector.expectedJWSAlg") + .isEqualTo(JWSAlgorithm.RS384); + }); + } + + @Test + void autoConfigurationUsingPublicKeyValueWithMultipleJwsAlgorithmsShouldFail() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RSA256,RS384").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseMessage( + "Creating a JWT decoder using a public key requires exactly one JWS algorithm but 2 were " + + "configured"); + }); + } + @Test @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index ebce6261c449..bb804edf6ce6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -33,6 +33,7 @@ import com.nimbusds.jose.JWSAlgorithm; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -55,6 +56,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; @@ -120,6 +122,7 @@ void autoConfigurationShouldMatchDefaultJwsAlgorithm() { } @Test + @Deprecated void autoConfigurationShouldConfigureResourceServerWithJwsAlgorithm() { this.contextRunner .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", @@ -134,6 +137,73 @@ void autoConfigurationShouldConfigureResourceServerWithJwsAlgorithm() { }); } + @Test + void autoConfigurationShouldConfigureResourceServerWithSingleJwsAlgorithm() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384") + .run((context) -> { + JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + Object processor = ReflectionTestUtils.getField(jwtDecoder, "jwtProcessor"); + Object keySelector = ReflectionTestUtils.getField(processor, "jwsKeySelector"); + assertThat(keySelector).extracting("jwsAlgs") + .asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class)) + .containsExactlyInAnyOrder(JWSAlgorithm.RS384); + assertThat(getBearerTokenFilter(context)).isNotNull(); + }); + } + + @Test + void autoConfigurationShouldConfigureResourceServerWithMultipleJwsAlgorithms() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS256, RS384, RS512") + .run((context) -> { + JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + Object processor = ReflectionTestUtils.getField(jwtDecoder, "jwtProcessor"); + Object keySelector = ReflectionTestUtils.getField(processor, "jwsKeySelector"); + assertThat(keySelector).extracting("jwsAlgs") + .asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class)) + .containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512); + assertThat(getBearerTokenFilter(context)).isNotNull(); + }); + } + + @Test + @Deprecated + void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingJwsAlgorithm() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.jws-algorithm=RS384").run((context) -> { + NimbusJwtDecoder nimbusJwtDecoder = context.getBean(NimbusJwtDecoder.class); + assertThat(nimbusJwtDecoder).extracting("jwtProcessor.jwsKeySelector.expectedJWSAlg") + .isEqualTo(JWSAlgorithm.RS384); + }); + } + + @Test + void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingSingleJwsAlgorithm() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384").run((context) -> { + NimbusJwtDecoder nimbusJwtDecoder = context.getBean(NimbusJwtDecoder.class); + assertThat(nimbusJwtDecoder).extracting("jwtProcessor.jwsKeySelector.expectedJWSAlg") + .isEqualTo(JWSAlgorithm.RS384); + }); + } + + @Test + void autoConfigurationUsingPublicKeyValueWithMultipleJwsAlgorithmsShouldFail() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.jws-algorithms=RSA256,RS384").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseMessage( + "Creating a JWT decoder using a public key requires exactly one JWS algorithm but 2 were " + + "configured"); + }); + } + @Test @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws Exception {