Allow multiple JWS algorithms to be configured
Closes gh-31321
This commit is contained in:
@@ -20,9 +20,11 @@ import java.io.IOException;
|
||||
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 class OAuth2ResourceServerProperties {
|
||||
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<String> jwsAlgorithms = Arrays.asList("RS256");
|
||||
|
||||
/**
|
||||
* URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0
|
||||
@@ -87,12 +89,23 @@ public class OAuth2ResourceServerProperties {
|
||||
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<String> getJwsAlgorithms() {
|
||||
return this.jwsAlgorithms;
|
||||
}
|
||||
|
||||
public void setJwsAlgorithms(List<String> jwsAlgortithms) {
|
||||
this.jwsAlgorithms = jwsAlgortithms;
|
||||
}
|
||||
|
||||
public String getIssuerUri() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
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 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
@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<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
|
||||
? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
|
||||
@@ -87,6 +87,12 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
return nimbusReactiveJwtDecoder;
|
||||
}
|
||||
|
||||
private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
|
||||
for (String algorithm : this.properties.getJwsAlgorithms()) {
|
||||
signatureAlgorithms.add(SignatureAlgorithm.from(algorithm));
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2TokenValidator<Jwt> getValidators(Supplier<OAuth2TokenValidator<Jwt>> defaultValidator) {
|
||||
OAuth2TokenValidator<Jwt> defaultValidators = defaultValidator.get();
|
||||
List<String> audiences = this.properties.getAudiences();
|
||||
@@ -106,7 +112,7 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
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 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
return Base64.getMimeDecoder().decode(keyValue);
|
||||
}
|
||||
|
||||
private String exactlyOneAlgorithm() {
|
||||
List<String> 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() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
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 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
@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<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
|
||||
? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
|
||||
@@ -86,6 +87,12 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
return nimbusJwtDecoder;
|
||||
}
|
||||
|
||||
private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
|
||||
for (String algorithm : this.properties.getJwsAlgorithms()) {
|
||||
signatureAlgorithms.add(SignatureAlgorithm.from(algorithm));
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2TokenValidator<Jwt> getValidators(Supplier<OAuth2TokenValidator<Jwt>> defaultValidator) {
|
||||
OAuth2TokenValidator<Jwt> defaultValidators = defaultValidator.get();
|
||||
List<String> audiences = this.properties.getAudiences();
|
||||
@@ -105,7 +112,7 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
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 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
return Base64.getMimeDecoder().decode(keyValue);
|
||||
}
|
||||
|
||||
private String exactlyOneAlgorithm() {
|
||||
List<String> 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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
|
||||
@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 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
}
|
||||
|
||||
@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 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.JwtClaimValidator;
|
||||
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 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated
|
||||
void autoConfigurationShouldConfigureResourceServerWithJwsAlgorithm() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
|
||||
@@ -134,6 +137,73 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
Reference in New Issue
Block a user