Skip to content

Commit 366146f

Browse files
committed
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
1 parent 2907864 commit 366146f

File tree

18 files changed

+199
-230
lines changed

18 files changed

+199
-230
lines changed

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ public void getWhenUsingDefaultsInLambdaWithValidBearerTokenThenAcceptsRequest()
223223
public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception {
224224
this.spring.register(WebServerConfig.class, JwkSetUriConfig.class, BasicController.class).autowire();
225225
mockWebServer(jwks("Default"));
226-
mockWebServer(jwks("Default"));
227226
String token = this.token("ValidNoScopes");
228227
// @formatter:off
229228
this.mvc.perform(get("/").with(bearerToken(token)))
@@ -236,7 +235,6 @@ public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception {
236235
public void getWhenUsingJwkSetUriInLambdaThenAcceptsRequest() throws Exception {
237236
this.spring.register(WebServerConfig.class, JwkSetUriInLambdaConfig.class, BasicController.class).autowire();
238237
mockWebServer(jwks("Default"));
239-
mockWebServer(jwks("Default"));
240238
String token = this.token("ValidNoScopes");
241239
// @formatter:off
242240
this.mvc.perform(get("/").with(bearerToken(token)))
@@ -1203,6 +1201,7 @@ public void getWhenMultipleIssuersThenUsesIssuerClaimToDifferentiate() throws Ex
12031201
// @formatter:on
12041202
mockWebServer(String.format(metadata, issuerThree, issuerThree));
12051203
mockWebServer(jwkSet);
1204+
mockWebServer(jwkSet);
12061205
// @formatter:off
12071206
this.mvc.perform(get("/authenticated").with(bearerToken(jwtThree)))
12081207
.andExpect(status().isUnauthorized())

config/src/test/java/org/springframework/security/config/http/OAuth2ResourceServerBeanDefinitionParserTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ public void getWhenValidBearerTokenThenAcceptsRequest() throws Exception {
148148
public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception {
149149
this.spring.configLocations(xml("WebServer"), xml("JwkSetUri")).autowire();
150150
mockWebServer(jwks("Default"));
151-
mockWebServer(jwks("Default"));
152151
String token = this.token("ValidNoScopes");
153152
// @formatter:off
154153
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))

config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ public void getWhenUsingJwkSetUriThenConsultsAccordingly() {
261261
this.spring.register(JwkSetUriConfig.class, RootController.class).autowire();
262262
MockWebServer mockWebServer = this.spring.getContext().getBean(MockWebServer.class);
263263
mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
264-
mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
265264
// @formatter:off
266265
this.client.get()
267266
.headers((headers) -> headers
@@ -277,7 +276,6 @@ public void getWhenUsingJwkSetUriInLambdaThenConsultsAccordingly() {
277276
this.spring.register(JwkSetUriInLambdaConfig.class, RootController.class).autowire();
278277
MockWebServer mockWebServer = this.spring.getContext().getBean(MockWebServer.class);
279278
mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
280-
mockWebServer.enqueue(new MockResponse().setBody(this.jwkSet));
281279
// @formatter:off
282280
this.client.get()
283281
.headers((headers) -> headers

config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ class ServerJwtDslTests {
160160
fun `jwt when using custom JWK Set URI then custom URI used`() {
161161
this.spring.register(CustomJwkSetUriConfig::class.java).autowire()
162162

163-
CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet))
164163
CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet))
165164

166165
this.client.get()

docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ When this property and these dependencies are used, Resource Server will automat
9999

100100
It achieves this through a deterministic startup process:
101101

102-
1. Hit the Provider Configuration or Authorization Server Metadata endpoint, processing the response for the `jwks_url` property
103-
2. Configure the validation strategy to query `jwks_url` for valid public keys
104-
3. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
102+
1. Query the Provider Configuration or Authorization Server Metadata endpoint for the `jwks_url` property
103+
2. Query the `jwks_url` endpoint for supported algorithms
104+
3. Configure the validation strategy to query `jwks_url` for valid public keys of the algorithms found
105+
4. Configure the validation strategy to validate each JWTs `iss` claim against `https://idp.example.com`.
105106

106107
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.
107108

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,25 @@
1818

1919
import java.net.URI;
2020
import java.util.Collections;
21+
import java.util.HashSet;
22+
import java.util.List;
2123
import java.util.Map;
24+
import java.util.Set;
25+
26+
import com.nimbusds.jose.JWSAlgorithm;
27+
import com.nimbusds.jose.KeySourceException;
28+
import com.nimbusds.jose.jwk.JWK;
29+
import com.nimbusds.jose.jwk.JWKMatcher;
30+
import com.nimbusds.jose.jwk.JWKSelector;
31+
import com.nimbusds.jose.jwk.KeyType;
32+
import com.nimbusds.jose.jwk.KeyUse;
33+
import com.nimbusds.jose.jwk.source.JWKSource;
34+
import com.nimbusds.jose.proc.SecurityContext;
2235

2336
import org.springframework.core.ParameterizedTypeReference;
2437
import org.springframework.http.RequestEntity;
2538
import org.springframework.http.ResponseEntity;
39+
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
2640
import org.springframework.util.Assert;
2741
import org.springframework.web.client.HttpClientErrorException;
2842
import org.springframework.web.client.RestTemplate;
@@ -68,6 +82,40 @@ static void validateIssuer(Map<String, Object> configuration, String issuer) {
6882
+ "\" provided in the configuration did not " + "match the requested issuer \"" + issuer + "\"");
6983
}
7084

85+
static Set<SignatureAlgorithm> getSignatureAlgorithms(JWKSource<SecurityContext> jwkSource) {
86+
JWKMatcher jwkMatcher = new JWKMatcher.Builder().publicOnly(true).keyUses(KeyUse.SIGNATURE, null)
87+
.keyTypes(KeyType.RSA, KeyType.EC).build();
88+
Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
89+
try {
90+
List<? extends JWK> jwks = jwkSource.get(new JWKSelector(jwkMatcher), null);
91+
for (JWK jwk : jwks) {
92+
if (jwk.getAlgorithm() != null) {
93+
jwsAlgorithms.add((JWSAlgorithm) jwk.getAlgorithm());
94+
}
95+
else {
96+
if (jwk.getKeyType() == KeyType.RSA) {
97+
jwsAlgorithms.addAll(JWSAlgorithm.Family.RSA);
98+
}
99+
else if (jwk.getKeyType() == KeyType.EC) {
100+
jwsAlgorithms.addAll(JWSAlgorithm.Family.EC);
101+
}
102+
}
103+
}
104+
}
105+
catch (KeySourceException ex) {
106+
throw new IllegalStateException(ex);
107+
}
108+
Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
109+
for (JWSAlgorithm jwsAlgorithm : jwsAlgorithms) {
110+
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(jwsAlgorithm.getName());
111+
if (signatureAlgorithm != null) {
112+
signatureAlgorithms.add(signatureAlgorithm);
113+
}
114+
}
115+
Assert.notEmpty(signatureAlgorithms, "Failed to find any algorithms from the JWK set");
116+
return signatureAlgorithms;
117+
}
118+
71119
private static String getMetadataIssuer(Map<String, Object> configuration) {
72120
if (configuration.containsKey("issuer")) {
73121
return configuration.get("issuer").toString();

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@
1616

1717
package org.springframework.security.oauth2.jwt;
1818

19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
21+
import java.net.URL;
1922
import java.util.Map;
23+
import java.util.Set;
24+
25+
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
26+
import com.nimbusds.jose.proc.SecurityContext;
2027

2128
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
29+
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
2230
import org.springframework.util.Assert;
2331

2432
/**
@@ -106,9 +114,23 @@ public static JwtDecoder fromIssuerLocation(String issuer) {
106114
private static JwtDecoder withProviderConfiguration(Map<String, Object> configuration, String issuer) {
107115
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
108116
OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
109-
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(configuration.get("jwks_uri").toString()).build();
117+
String jwkSetUri = configuration.get("jwks_uri").toString();
118+
RemoteJWKSet<SecurityContext> jwkSource = new RemoteJWKSet<>(url(jwkSetUri));
119+
Set<SignatureAlgorithm> signatureAlgorithms = JwtDecoderProviderConfigurationUtils
120+
.getSignatureAlgorithms(jwkSource);
121+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
122+
.jwsAlgorithms((algs) -> algs.addAll(signatureAlgorithms)).build();
110123
jwtDecoder.setJwtValidator(jwtValidator);
111124
return jwtDecoder;
112125
}
113126

127+
private static URL url(String url) {
128+
try {
129+
return new URL(url);
130+
}
131+
catch (IOException ex) {
132+
throw new UncheckedIOException(ex);
133+
}
134+
}
135+
114136
}

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java

Lines changed: 5 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,21 @@
2121
import java.net.URL;
2222
import java.security.interfaces.RSAPublicKey;
2323
import java.text.ParseException;
24-
import java.util.ArrayList;
2524
import java.util.Arrays;
2625
import java.util.Collection;
2726
import java.util.Collections;
2827
import java.util.HashSet;
2928
import java.util.LinkedHashMap;
30-
import java.util.List;
3129
import java.util.Map;
3230
import java.util.Set;
3331
import java.util.function.Consumer;
3432

3533
import javax.crypto.SecretKey;
3634

37-
import com.nimbusds.jose.Algorithm;
3835
import com.nimbusds.jose.JOSEException;
3936
import com.nimbusds.jose.JWSAlgorithm;
4037
import com.nimbusds.jose.RemoteKeySourceException;
41-
import com.nimbusds.jose.jwk.JWK;
4238
import com.nimbusds.jose.jwk.JWKSet;
43-
import com.nimbusds.jose.jwk.KeyUse;
4439
import com.nimbusds.jose.jwk.source.JWKSetCache;
4540
import com.nimbusds.jose.jwk.source.JWKSource;
4641
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
@@ -239,8 +234,6 @@ public static SecretKeyJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
239234
*/
240235
public static final class JwkSetUriJwtDecoderBuilder {
241236

242-
private static final Log log = LogFactory.getLog(JwkSetUriJwtDecoderBuilder.class);
243-
244237
private String jwkSetUri;
245238

246239
private Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
@@ -329,60 +322,17 @@ public JwkSetUriJwtDecoderBuilder jwtProcessorCustomizer(
329322
}
330323

331324
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
332-
Set<SignatureAlgorithm> algorithms = new HashSet<>();
333-
if (!this.signatureAlgorithms.isEmpty()) {
334-
algorithms.addAll(this.signatureAlgorithms);
335-
} else {
336-
algorithms.addAll(fetchSignatureAlgorithms());
337-
}
338-
339-
if (algorithms.isEmpty()) {
340-
algorithms.add(SignatureAlgorithm.RS256);
325+
if (this.signatureAlgorithms.isEmpty()) {
326+
return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource);
341327
}
342-
343328
Set<JWSAlgorithm> jwsAlgorithms = new HashSet<>();
344-
for (SignatureAlgorithm signatureAlgorithm : algorithms) {
345-
jwsAlgorithms.add(JWSAlgorithm.parse(signatureAlgorithm.getName()));
329+
for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) {
330+
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
331+
jwsAlgorithms.add(jwsAlgorithm);
346332
}
347-
348333
return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource);
349334
}
350335

351-
private Set<SignatureAlgorithm> fetchSignatureAlgorithms() {
352-
try {
353-
return parseAlgorithms(JWKSet.load(toURL(jwkSetUri), 5000, 5000, 0));
354-
} catch (Exception ex) {
355-
throw new IllegalArgumentException("Failed to load Signature Algorithms from remote JWK source.", ex);
356-
}
357-
}
358-
359-
private Set<SignatureAlgorithm> parseAlgorithms(JWKSet jwkSet) {
360-
if (jwkSet == null) {
361-
throw new IllegalArgumentException(String.format("No JWKs received from %s", jwkSetUri));
362-
}
363-
364-
List<JWK> jwks = new ArrayList<>();
365-
for (JWK jwk : jwkSet.getKeys()) {
366-
KeyUse keyUse = jwk.getKeyUse();
367-
if (keyUse != null && keyUse.equals(KeyUse.SIGNATURE)) {
368-
jwks.add(jwk);
369-
}
370-
}
371-
372-
Set<SignatureAlgorithm> algorithms = new HashSet<>();
373-
for (JWK jwk : jwks) {
374-
Algorithm algorithm = jwk.getAlgorithm();
375-
if (algorithm != null) {
376-
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(algorithm.getName());
377-
if (signatureAlgorithm != null) {
378-
algorithms.add(signatureAlgorithm);
379-
}
380-
}
381-
}
382-
383-
return algorithms;
384-
}
385-
386336
JWKSource<SecurityContext> jwkSource(ResourceRetriever jwkSetRetriever) {
387337
if (this.cache == null) {
388338
return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever);

0 commit comments

Comments
 (0)