Skip to content

Commit 9e3cb1e

Browse files
committed
JwkTokenStore Supports Multiple JWK Set URLs
Fixes spring-atticgh-1050
1 parent 64d7651 commit 9e3cb1e

File tree

5 files changed

+136
-12
lines changed

5 files changed

+136
-12
lines changed

spring-security-oauth2/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@
193193
<optional>true</optional>
194194
</dependency>
195195

196+
<dependency>
197+
<groupId>com.squareup.okhttp3</groupId>
198+
<artifactId>mockwebserver</artifactId>
199+
<version>3.7.0</version>
200+
<scope>test</scope>
201+
</dependency>
202+
196203
<dependency>
197204
<groupId>junit</groupId>
198205
<artifactId>junit</artifactId>

spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/token/store/jwk/JwkDefinitionSource.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@
2727
import java.security.KeyFactory;
2828
import java.security.interfaces.RSAPublicKey;
2929
import java.security.spec.RSAPublicKeySpec;
30-
import java.util.LinkedHashMap;
31-
import java.util.Map;
32-
import java.util.Set;
30+
import java.util.*;
3331
import java.util.concurrent.ConcurrentHashMap;
3432

3533
/**
@@ -44,7 +42,7 @@
4442
* @author Joe Grandja
4543
*/
4644
class JwkDefinitionSource {
47-
private final URL jwkSetUrl;
45+
private final List<URL> jwkSetUrls;
4846
private final Map<String, JwkDefinitionHolder> jwkDefinitions = new ConcurrentHashMap<String, JwkDefinitionHolder>();
4947
private static final JwkSetConverter jwkSetConverter = new JwkSetConverter();
5048

@@ -54,10 +52,22 @@ class JwkDefinitionSource {
5452
* @param jwkSetUrl the JWK Set URL
5553
*/
5654
JwkDefinitionSource(String jwkSetUrl) {
57-
try {
58-
this.jwkSetUrl = new URL(jwkSetUrl);
59-
} catch (MalformedURLException ex) {
60-
throw new IllegalArgumentException("Invalid JWK Set URL: " + ex.getMessage(), ex);
55+
this(Arrays.asList(jwkSetUrl));
56+
}
57+
58+
/**
59+
* Creates a new instance using the provided URLs as the location for the JWK Sets.
60+
*
61+
* @param jwkSetUrls the JWK Set URLs
62+
*/
63+
JwkDefinitionSource(List<String> jwkSetUrls) {
64+
this.jwkSetUrls = new ArrayList<URL>();
65+
for(String jwkSetUrl : jwkSetUrls) {
66+
try {
67+
this.jwkSetUrls.add(new URL(jwkSetUrl));
68+
} catch (MalformedURLException ex) {
69+
throw new IllegalArgumentException("Invalid JWK Set URL: " + ex.getMessage(), ex);
70+
}
6171
}
6272
}
6373

@@ -90,7 +100,9 @@ JwkDefinition getDefinitionLoadIfNecessary(String keyId) {
90100
return result;
91101
}
92102
this.jwkDefinitions.clear();
93-
this.jwkDefinitions.putAll(loadJwkDefinitions(this.jwkSetUrl));
103+
for(URL jwkSetUrl : jwkSetUrls) {
104+
this.jwkDefinitions.putAll(loadJwkDefinitions(jwkSetUrl));
105+
}
94106
return this.getDefinition(keyId);
95107
}
96108

spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/token/store/jwk/JwkTokenStore.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
2424
import org.springframework.util.Assert;
2525

26+
import java.util.Arrays;
2627
import java.util.Collection;
28+
import java.util.List;
2729

2830
/**
2931
* A {@link TokenStore} implementation that provides support for verifying the
@@ -86,16 +88,23 @@
8688
* @author Joe Grandja
8789
*/
8890
public final class JwkTokenStore implements TokenStore {
89-
private final JwtTokenStore delegate;
91+
private final TokenStore delegate;
9092

9193
/**
9294
* Creates a new instance using the provided URL as the location for the JWK Set.
9395
*
9496
* @param jwkSetUrl the JWK Set URL
9597
*/
9698
public JwkTokenStore(String jwkSetUrl) {
97-
Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
98-
JwkDefinitionSource jwkDefinitionSource = new JwkDefinitionSource(jwkSetUrl);
99+
this(Arrays.asList(jwkSetUrl));
100+
}
101+
102+
/**
103+
* Creates a new instance using the provided URLs as the location for the JWK Sets.
104+
* @param jwkSetUrls the JWK Set URLs
105+
*/
106+
public JwkTokenStore(List<String> jwkSetUrls) {
107+
JwkDefinitionSource jwkDefinitionSource = new JwkDefinitionSource(jwkSetUrls);
99108
JwkVerifyingJwtAccessTokenConverter accessTokenConverter =
100109
new JwkVerifyingJwtAccessTokenConverter(jwkDefinitionSource);
101110
this.delegate = new JwtTokenStore(accessTokenConverter);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.provider.token.store.jwk;
17+
18+
import okhttp3.mockwebserver.MockResponse;
19+
import okhttp3.mockwebserver.MockWebServer;
20+
import org.apache.http.HttpHeaders;
21+
import org.junit.Before;
22+
import org.junit.Test;
23+
import org.springframework.http.MediaType;
24+
25+
import java.util.Arrays;
26+
27+
import static org.junit.Assert.assertEquals;
28+
29+
/**
30+
* @author Rob Winch
31+
*/
32+
public class JwkDefinitionSourceITest {
33+
34+
private MockWebServer server;
35+
36+
private JwkDefinitionSource source;
37+
38+
@Before
39+
public void setup() {
40+
this.server = new MockWebServer();
41+
}
42+
43+
@Test
44+
public void getDefinitionLoadIfNecessaryWhenMultipleUrlsThenBothUrlsAreLoaded() {
45+
this.server.enqueue(new MockResponse().setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).setBody("{\n" +
46+
" \"keys\": [\n" +
47+
" {\n" +
48+
" \"kid\": \"key-id-1\",\n" +
49+
" \"kty\": \"RSA\",\n" +
50+
" \"alg\": \"RS256\",\n" +
51+
" \"use\": \"sig\",\n" +
52+
" \"n\": \"rne3dowbQHcFCzg2ejWb6az5QNxWFiv6kRpd34VDzYNMhWeewfeEL5Pf5clE8Xh1KlllrDYSxtnzUQm-t9p92yEBASfV96ydTYG-ITfxfJzKtJUN-iIS5K9WGYXnDNS4eYZ_ygW-zBU_9NwFMXdwSTzRqHeJmLJrfbmmjoIuuWyfh2Ko52KzyidceR5SJxGeW0ckeyWka1lDf4cr7fv-s093Y_sd2wrNvg0-9IAkXotbxWWXcfMgXFyw0qHFT_5LrKmiwkY3HCaiV5NgEFJmC6fBIG2EOZG4rqjBoYV6LZwrfTMHknaeel9MOZesW6SR2bswtuuWN3DGq2zg0KamLw\",\n" +
53+
" \"e\": \"AQAB\"\n" +
54+
" }\n" +
55+
" ]\n" +
56+
"}\n"));
57+
this.server.enqueue(new MockResponse().setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE).setBody("{\n" +
58+
" \"keys\": [\n" +
59+
" {\n" +
60+
" \"kid\": \"key-id-2\",\n" +
61+
" \"kty\": \"RSA\",\n" +
62+
" \"alg\": \"RS256\",\n" +
63+
" \"use\": \"sig\",\n" +
64+
" \"n\": \"t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRyO125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0XOC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q\",\n" +
65+
" \"e\": \"AQAB\"\n" +
66+
" }\n" +
67+
" ]\n" +
68+
"}\n"));
69+
this.source = new JwkDefinitionSource(Arrays.asList(serverUrl("/jwk1"), serverUrl("/jkw2")));
70+
71+
String keyId1 = "key-id-1";
72+
String keyId2 = "key-id-2";
73+
JwkDefinition jwkDef1 = this.source.getDefinitionLoadIfNecessary(keyId1);
74+
JwkDefinition jwkDef2 = this.source.getDefinitionLoadIfNecessary(keyId2);
75+
76+
assertEquals(jwkDef1.getKeyId(), keyId1);
77+
assertEquals(jwkDef1.getAlgorithm(), JwkDefinition.CryptoAlgorithm.RS256);
78+
assertEquals(jwkDef1.getPublicKeyUse(), JwkDefinition.PublicKeyUse.SIG);
79+
assertEquals(jwkDef1.getKeyType(), JwkDefinition.KeyType.RSA);
80+
81+
assertEquals(jwkDef2.getKeyId(), keyId2);
82+
assertEquals(jwkDef2.getAlgorithm(), JwkDefinition.CryptoAlgorithm.RS256);
83+
assertEquals(jwkDef2.getPublicKeyUse(), JwkDefinition.PublicKeyUse.SIG);
84+
assertEquals(jwkDef2.getKeyType(), JwkDefinition.KeyType.RSA);
85+
}
86+
87+
private String serverUrl(String path) {
88+
return this.server.url(path).toString();
89+
}
90+
}

spring-security-oauth2/src/test/java/org/springframework/security/oauth2/provider/token/store/jwk/JwkDefinitionSourceTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.io.IOException;
2727
import java.io.InputStream;
2828
import java.net.URL;
29+
import java.util.Arrays;
2930
import java.util.Collections;
3031

3132
import static org.mockito.Matchers.any;
@@ -46,6 +47,11 @@ public void constructorWhenInvalidJwkSetUrlThenThrowIllegalArgumentException() t
4647
new JwkDefinitionSource(DEFAULT_JWK_SET_URL.substring(1));
4748
}
4849

50+
@Test(expected = IllegalArgumentException.class)
51+
public void constructorListWhenInvalidJwkSetUrlThenThrowIllegalArgumentException() throws Exception {
52+
new JwkDefinitionSource(Arrays.asList(DEFAULT_JWK_SET_URL.substring(1)));
53+
}
54+
4955
@Test
5056
public void getDefinitionLoadIfNecessaryWhenKeyIdNotFoundThenLoadJwkDefinitions() throws Exception {
5157
JwkDefinitionSource jwkDefinitionSource = spy(new JwkDefinitionSource(DEFAULT_JWK_SET_URL));

0 commit comments

Comments
 (0)