Skip to content

Commit a1cc5bf

Browse files
committed
Allow multiple JWS algorithms to be configured
Closes gh-31321
1 parent 5e1cd28 commit a1cc5bf

File tree

6 files changed

+184
-9
lines changed

6 files changed

+184
-9
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
import java.io.InputStream;
2121
import java.nio.charset.StandardCharsets;
2222
import java.util.ArrayList;
23+
import java.util.Arrays;
2324
import java.util.List;
2425

2526
import org.springframework.boot.context.properties.ConfigurationProperties;
27+
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
2628
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
2729
import org.springframework.core.io.Resource;
2830
import org.springframework.util.Assert;
@@ -59,9 +61,9 @@ public static class Jwt {
5961
private String jwkSetUri;
6062

6163
/**
62-
* JSON Web Algorithm used for verifying the digital signatures.
64+
* JSON Web Algorithms used for verifying the digital signatures.
6365
*/
64-
private String jwsAlgorithm = "RS256";
66+
private List<String> jwsAlgorithms = Arrays.asList("RS256");
6567

6668
/**
6769
* URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0
@@ -87,12 +89,23 @@ public void setJwkSetUri(String jwkSetUri) {
8789
this.jwkSetUri = jwkSetUri;
8890
}
8991

92+
@Deprecated
93+
@DeprecatedConfigurationProperty(replacement = "spring.security.oauth2.resourceserver.jwt.jws-algorithms")
9094
public String getJwsAlgorithm() {
91-
return this.jwsAlgorithm;
95+
return this.jwsAlgorithms.isEmpty() ? null : this.jwsAlgorithms.get(0);
9296
}
9397

98+
@Deprecated
9499
public void setJwsAlgorithm(String jwsAlgorithm) {
95-
this.jwsAlgorithm = jwsAlgorithm;
100+
this.jwsAlgorithms = new ArrayList<>(Arrays.asList(jwsAlgorithm));
101+
}
102+
103+
public List<String> getJwsAlgorithms() {
104+
return this.jwsAlgorithms;
105+
}
106+
107+
public void setJwsAlgorithms(List<String> jwsAlgortithms) {
108+
this.jwsAlgorithms = jwsAlgortithms;
96109
}
97110

98111
public String getIssuerUri() {

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Base64;
2424
import java.util.Collections;
2525
import java.util.List;
26+
import java.util.Set;
2627
import java.util.function.Supplier;
2728

2829
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -78,15 +79,20 @@ static class JwtConfiguration {
7879
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
7980
ReactiveJwtDecoder jwtDecoder() {
8081
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder
81-
.withJwkSetUri(this.properties.getJwkSetUri())
82-
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
82+
.withJwkSetUri(this.properties.getJwkSetUri()).jwsAlgorithms(this::jwsAlgorithms).build();
8383
String issuerUri = this.properties.getIssuerUri();
8484
Supplier<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
8585
? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
8686
nimbusReactiveJwtDecoder.setJwtValidator(getValidators(defaultValidator));
8787
return nimbusReactiveJwtDecoder;
8888
}
8989

90+
private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
91+
for (String algorithm : this.properties.getJwsAlgorithms()) {
92+
signatureAlgorithms.add(SignatureAlgorithm.from(algorithm));
93+
}
94+
}
95+
9096
private OAuth2TokenValidator<Jwt> getValidators(Supplier<OAuth2TokenValidator<Jwt>> defaultValidator) {
9197
OAuth2TokenValidator<Jwt> defaultValidators = defaultValidator.get();
9298
List<String> audiences = this.properties.getAudiences();
@@ -106,7 +112,7 @@ NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
106112
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
107113
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
108114
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey)
109-
.signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
115+
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())).build();
110116
jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault));
111117
return jwtDecoder;
112118
}
@@ -116,6 +122,17 @@ private byte[] getKeySpec(String keyValue) {
116122
return Base64.getMimeDecoder().decode(keyValue);
117123
}
118124

125+
private String exactlyOneAlgorithm() {
126+
List<String> algorithms = this.properties.getJwsAlgorithms();
127+
int count = (algorithms != null) ? algorithms.size() : 0;
128+
if (count != 1) {
129+
throw new IllegalStateException(
130+
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count
131+
+ " were configured");
132+
}
133+
return algorithms.get(0);
134+
}
135+
119136
@Bean
120137
@Conditional(IssuerUriCondition.class)
121138
SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() {

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Base64;
2424
import java.util.Collections;
2525
import java.util.List;
26+
import java.util.Set;
2627
import java.util.function.Supplier;
2728

2829
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -78,14 +79,20 @@ static class JwtDecoderConfiguration {
7879
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
7980
JwtDecoder jwtDecoderByJwkKeySetUri() {
8081
NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
81-
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
82+
.jwsAlgorithms(this::jwsAlgorithms).build();
8283
String issuerUri = this.properties.getIssuerUri();
8384
Supplier<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
8485
? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
8586
nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator));
8687
return nimbusJwtDecoder;
8788
}
8889

90+
private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
91+
for (String algorithm : this.properties.getJwsAlgorithms()) {
92+
signatureAlgorithms.add(SignatureAlgorithm.from(algorithm));
93+
}
94+
}
95+
8996
private OAuth2TokenValidator<Jwt> getValidators(Supplier<OAuth2TokenValidator<Jwt>> defaultValidator) {
9097
OAuth2TokenValidator<Jwt> defaultValidators = defaultValidator.get();
9198
List<String> audiences = this.properties.getAudiences();
@@ -105,7 +112,7 @@ JwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
105112
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
106113
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
107114
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey)
108-
.signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
115+
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())).build();
109116
jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault));
110117
return jwtDecoder;
111118
}
@@ -115,6 +122,17 @@ private byte[] getKeySpec(String keyValue) {
115122
return Base64.getMimeDecoder().decode(keyValue);
116123
}
117124

125+
private String exactlyOneAlgorithm() {
126+
List<String> algorithms = this.properties.getJwsAlgorithms();
127+
int count = (algorithms != null) ? algorithms.size() : 0;
128+
if (count != 1) {
129+
throw new IllegalStateException(
130+
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count
131+
+ " were configured");
132+
}
133+
return algorithms.get(0);
134+
}
135+
118136
@Bean
119137
@Conditional(IssuerUriCondition.class)
120138
SupplierJwtDecoder jwtDecoderByIssuerUri() {

spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,6 +2058,11 @@
20582058
"name": "spring.security.filter.order",
20592059
"defaultValue": -100
20602060
},
2061+
{
2062+
"name": "spring.security.oauth2.resourceserver.jwt.jws-algorithm",
2063+
"description": "JSON Web Algorithm used for verifying the digital signatures.",
2064+
"defaultValue": "RS256"
2065+
},
20612066
{
20622067
"name": "spring.session.hazelcast.flush-mode",
20632068
"defaultValue": "on-save"

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.nimbusds.jose.JWSAlgorithm;
3333
import okhttp3.mockwebserver.MockResponse;
3434
import okhttp3.mockwebserver.MockWebServer;
35+
import org.assertj.core.api.InstanceOfAssertFactories;
3536
import org.junit.jupiter.api.AfterEach;
3637
import org.junit.jupiter.api.Test;
3738
import reactor.core.publisher.Mono;
@@ -114,6 +115,7 @@ void autoConfigurationShouldConfigureResourceServer() {
114115

115116
@SuppressWarnings("unchecked")
116117
@Test
118+
@Deprecated
117119
void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingJwsAlgorithm() {
118120
this.contextRunner
119121
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
@@ -126,6 +128,33 @@ void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingJwsAlgorit
126128
}
127129

128130
@Test
131+
void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingSingleJwsAlgorithm() {
132+
this.contextRunner
133+
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
134+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS512")
135+
.run((context) -> {
136+
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class);
137+
assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$2.arg$1.jwsAlgs")
138+
.asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class))
139+
.containsExactlyInAnyOrder(JWSAlgorithm.RS512);
140+
});
141+
}
142+
143+
@Test
144+
void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingMultipleJwsAlgorithms() {
145+
this.contextRunner
146+
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
147+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS256, RS384, RS512")
148+
.run((context) -> {
149+
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class);
150+
assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$2.arg$1.jwsAlgs")
151+
.asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class))
152+
.containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512);
153+
});
154+
}
155+
156+
@Test
157+
@Deprecated
129158
void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingJwsAlgorithm() {
130159
this.contextRunner.withPropertyValues(
131160
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
@@ -136,6 +165,29 @@ void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingJwsAl
136165
});
137166
}
138167

168+
@Test
169+
void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingSingleJwsAlgorithm() {
170+
this.contextRunner.withPropertyValues(
171+
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
172+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384").run((context) -> {
173+
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class);
174+
assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$1.jwsKeySelector.expectedJWSAlg")
175+
.isEqualTo(JWSAlgorithm.RS384);
176+
});
177+
}
178+
179+
@Test
180+
void autoConfigurationUsingPublicKeyValueWithMultipleJwsAlgorithmsShouldFail() {
181+
this.contextRunner.withPropertyValues(
182+
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
183+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RSA256,RS384").run((context) -> {
184+
assertThat(context).hasFailed();
185+
assertThat(context.getStartupFailure()).hasRootCauseMessage(
186+
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but 2 were "
187+
+ "configured");
188+
});
189+
}
190+
139191
@Test
140192
@SuppressWarnings("unchecked")
141193
void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException {

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.nimbusds.jose.JWSAlgorithm;
3434
import okhttp3.mockwebserver.MockResponse;
3535
import okhttp3.mockwebserver.MockWebServer;
36+
import org.assertj.core.api.InstanceOfAssertFactories;
3637
import org.junit.jupiter.api.AfterEach;
3738
import org.junit.jupiter.api.Test;
3839

@@ -55,6 +56,7 @@
5556
import org.springframework.security.oauth2.jwt.JwtDecoder;
5657
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
5758
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
59+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
5860
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
5961
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
6062
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
@@ -120,6 +122,7 @@ void autoConfigurationShouldMatchDefaultJwsAlgorithm() {
120122
}
121123

122124
@Test
125+
@Deprecated
123126
void autoConfigurationShouldConfigureResourceServerWithJwsAlgorithm() {
124127
this.contextRunner
125128
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
@@ -134,6 +137,73 @@ void autoConfigurationShouldConfigureResourceServerWithJwsAlgorithm() {
134137
});
135138
}
136139

140+
@Test
141+
void autoConfigurationShouldConfigureResourceServerWithSingleJwsAlgorithm() {
142+
this.contextRunner
143+
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
144+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384")
145+
.run((context) -> {
146+
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
147+
Object processor = ReflectionTestUtils.getField(jwtDecoder, "jwtProcessor");
148+
Object keySelector = ReflectionTestUtils.getField(processor, "jwsKeySelector");
149+
assertThat(keySelector).extracting("jwsAlgs")
150+
.asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class))
151+
.containsExactlyInAnyOrder(JWSAlgorithm.RS384);
152+
assertThat(getBearerTokenFilter(context)).isNotNull();
153+
});
154+
}
155+
156+
@Test
157+
void autoConfigurationShouldConfigureResourceServerWithMultipleJwsAlgorithms() {
158+
this.contextRunner
159+
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
160+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS256, RS384, RS512")
161+
.run((context) -> {
162+
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
163+
Object processor = ReflectionTestUtils.getField(jwtDecoder, "jwtProcessor");
164+
Object keySelector = ReflectionTestUtils.getField(processor, "jwsKeySelector");
165+
assertThat(keySelector).extracting("jwsAlgs")
166+
.asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class))
167+
.containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512);
168+
assertThat(getBearerTokenFilter(context)).isNotNull();
169+
});
170+
}
171+
172+
@Test
173+
@Deprecated
174+
void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingJwsAlgorithm() {
175+
this.contextRunner.withPropertyValues(
176+
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
177+
"spring.security.oauth2.resourceserver.jwt.jws-algorithm=RS384").run((context) -> {
178+
NimbusJwtDecoder nimbusJwtDecoder = context.getBean(NimbusJwtDecoder.class);
179+
assertThat(nimbusJwtDecoder).extracting("jwtProcessor.jwsKeySelector.expectedJWSAlg")
180+
.isEqualTo(JWSAlgorithm.RS384);
181+
});
182+
}
183+
184+
@Test
185+
void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingSingleJwsAlgorithm() {
186+
this.contextRunner.withPropertyValues(
187+
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
188+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384").run((context) -> {
189+
NimbusJwtDecoder nimbusJwtDecoder = context.getBean(NimbusJwtDecoder.class);
190+
assertThat(nimbusJwtDecoder).extracting("jwtProcessor.jwsKeySelector.expectedJWSAlg")
191+
.isEqualTo(JWSAlgorithm.RS384);
192+
});
193+
}
194+
195+
@Test
196+
void autoConfigurationUsingPublicKeyValueWithMultipleJwsAlgorithmsShouldFail() {
197+
this.contextRunner.withPropertyValues(
198+
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
199+
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RSA256,RS384").run((context) -> {
200+
assertThat(context).hasFailed();
201+
assertThat(context.getStartupFailure()).hasRootCauseMessage(
202+
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but 2 were "
203+
+ "configured");
204+
});
205+
}
206+
137207
@Test
138208
@SuppressWarnings("unchecked")
139209
void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws Exception {

0 commit comments

Comments
 (0)