Skip to content

Commit dbec0df

Browse files
committed
JWK Support
This commit proves Resource Server's existing support for JWT validation using a JWK set url as already supported by NimbusJwtDecoderJwkSupport. No functionality is added to support this feature as it was already available through components development for OAuth2 Client. Fixes: spring-projects/spring-security#5130
1 parent f71a965 commit dbec0df

File tree

6 files changed

+711
-0
lines changed

6 files changed

+711
-0
lines changed

gradle/dependency-management.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ dependencyManagement {
66

77
dependencies {
88
dependency 'com.auth0:java-jwt:3.3.0'
9+
dependency 'com.squareup.okhttp3:mockwebserver:3.7.0'
910
dependency 'commons-io:commons-io:2.6'
1011
dependency 'javax.servlet:javax.servlet-api:4.0.0'
1112
dependency 'junit:junit:4.12'

oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle

+5
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ dependencies {
66
compile 'org.springframework.security:spring-security-web'
77

88
compile 'javax.servlet:javax.servlet-api'
9+
10+
testCompile 'org.springframework.security:spring-security-test'
11+
testCompile 'org.springframework:spring-webmvc'
12+
13+
testCompile 'com.squareup.okhttp3:mockwebserver'
914
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Copyright 2002-2018 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.config.annotation.web.configurers.oauth2.resourceserver;
17+
18+
import okhttp3.HttpUrl;
19+
import okhttp3.mockwebserver.MockResponse;
20+
import okhttp3.mockwebserver.MockWebServer;
21+
import org.junit.Test;
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.mock.web.MockServletConfig;
28+
import org.springframework.mock.web.MockServletContext;
29+
import org.springframework.security.authentication.AuthenticationProvider;
30+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
31+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
32+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
33+
import org.springframework.security.oauth2.jose.jwk.JwkSetBuilder;
34+
import org.springframework.security.oauth2.jose.jws.JwsBuilder;
35+
import org.springframework.security.oauth2.jwt.JwtDecoder;
36+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
37+
import org.springframework.security.oauth2.resourceserver.authentication.JwtEncodedOAuth2AccessTokenAuthenticationProvider;
38+
import org.springframework.security.oauth2.resourceserver.web.BearerTokenAuthenticationFilter;
39+
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
40+
import org.springframework.test.web.servlet.MockMvc;
41+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
42+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
43+
import org.springframework.web.bind.annotation.GetMapping;
44+
import org.springframework.web.bind.annotation.RestController;
45+
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
46+
47+
import java.time.Instant;
48+
import java.time.temporal.ChronoUnit;
49+
50+
import static org.assertj.core.api.Assertions.assertThat;
51+
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
52+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
53+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
54+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
55+
56+
/**
57+
* @author Josh Cummings
58+
*/
59+
public class ResourceServerConfigurerTests {
60+
61+
private AnnotationConfigWebApplicationContext context;
62+
63+
private MockMvc mvc;
64+
private MockWebServer server;
65+
66+
@Test
67+
public void performWhenBearerIsSignedWithJwkOverRsaThenAccessIsAuthorized()
68+
throws Exception {
69+
70+
this.register(WebServerConfig.class, RsaConfig.class);
71+
72+
JwkSetBuilder good = new JwkSetBuilder().rsa("good");
73+
74+
String authority = new JwsBuilder("RS256")
75+
.expiresAt(Instant.now().plus(1, ChronoUnit.HOURS))
76+
.scope("permission.read")
77+
.signWithAny(good).build();
78+
79+
this.setupJwks(good);
80+
81+
this.mvc.perform(bearer(get("/"), authority))
82+
.andExpect(content().string("OK"));
83+
84+
assertThat(this.server.getRequestCount()).isEqualTo(1);
85+
}
86+
87+
@Test
88+
public void performWhenBearerIsSignedWithMissingJwkOverRsaThenNotAuthorized()
89+
throws Exception {
90+
91+
this.register(WebServerConfig.class, RsaConfig.class);
92+
93+
JwkSetBuilder bad = new JwkSetBuilder().rsa("bad");
94+
JwkSetBuilder good = new JwkSetBuilder().rsa("good");
95+
96+
String authority = new JwsBuilder("RS256").signWithAny(bad).build();
97+
98+
this.setupJwks(good);
99+
100+
this.mvc.perform(bearer(get("/"), authority))
101+
.andExpect(status().isUnauthorized());
102+
103+
assertThat(this.server.getRequestCount()).isEqualTo(2);
104+
}
105+
106+
@Test
107+
public void performWhenBearerIsSignedButServerHasNoJwksThenNotAuthorized()
108+
throws Exception {
109+
110+
this.register(WebServerConfig.class, RsaConfig.class);
111+
112+
JwkSetBuilder empty = new JwkSetBuilder();
113+
JwkSetBuilder good = new JwkSetBuilder().rsa("good");
114+
115+
String authority = new JwsBuilder("RS256").signWithAny(good).build();
116+
117+
this.setupJwks(empty);
118+
119+
this.mvc.perform(bearer(get("/"), authority))
120+
.andExpect(status().isUnauthorized());
121+
122+
assertThat(this.server.getRequestCount()).isEqualTo(2);
123+
}
124+
125+
@EnableWebSecurity
126+
public static class RsaConfig extends WithResourceServerConfigurerAdapter {
127+
@Autowired
128+
MockWebServer server;
129+
130+
@Override
131+
protected void configure(HttpSecurity http) throws Exception {
132+
super.configure(http);
133+
134+
http
135+
.authorizeRequests().anyRequest()
136+
.access("authentication.hasClaim('scope', 'permission.read')");
137+
}
138+
139+
@Bean
140+
protected AuthenticationProvider oauth2AuthenticationProvider() {
141+
return new JwtEncodedOAuth2AccessTokenAuthenticationProvider(this.jwtDecoder());
142+
}
143+
144+
@Bean
145+
JwtDecoder jwtDecoder() {
146+
HttpUrl url = this.server.url("/.well-known/jwks.json");
147+
148+
return new NimbusJwtDecoderJwkSupport(
149+
url.toString(),
150+
"RS256");
151+
}
152+
153+
}
154+
155+
@Test
156+
public void performWhenBearerIsSignedWithJwkOverEcdsaThenAuthorized()
157+
throws Exception {
158+
159+
this.register(WebServerConfig.class, EcConfig.class);
160+
161+
JwkSetBuilder good = new JwkSetBuilder().ec("good");
162+
163+
String authority = new JwsBuilder("ES512")
164+
.expiresAt(Instant.now().plus(1, ChronoUnit.HOURS))
165+
.scope("permission.read")
166+
.signWithAny(good).build();
167+
168+
this.setupJwks(good);
169+
170+
this.mvc.perform(bearer(get("/"), authority))
171+
.andExpect(content().string("OK"));
172+
173+
assertThat(this.server.getRequestCount()).isEqualTo(1);
174+
}
175+
176+
@EnableWebSecurity
177+
public static class EcConfig extends WithResourceServerConfigurerAdapter {
178+
@Autowired MockWebServer server;
179+
180+
@Override
181+
protected void configure(HttpSecurity http) throws Exception {
182+
super.configure(http);
183+
184+
http
185+
.authorizeRequests().anyRequest()
186+
.access("authentication.hasClaim('scope', 'permission.read')");
187+
}
188+
189+
@Bean
190+
protected AuthenticationProvider oauth2AuthenticationProvider() {
191+
return new JwtEncodedOAuth2AccessTokenAuthenticationProvider(this.jwtDecoder());
192+
}
193+
194+
@Bean
195+
JwtDecoder jwtDecoder() {
196+
HttpUrl url = this.server.url("/.well-known/jwks.json");
197+
198+
return new NimbusJwtDecoderJwkSupport(
199+
url.toString(),
200+
"ES512");
201+
}
202+
203+
}
204+
205+
public static abstract class WithResourceServerConfigurerAdapter extends WebSecurityConfigurerAdapter {
206+
@Override
207+
protected void configure(HttpSecurity http) throws Exception {
208+
http
209+
.addFilterAfter(
210+
new BearerTokenAuthenticationFilter(authenticationManager()),
211+
BasicAuthenticationFilter.class)
212+
.authenticationProvider(oauth2AuthenticationProvider())
213+
.exceptionHandling().and()
214+
.csrf().disable();
215+
}
216+
217+
protected abstract AuthenticationProvider oauth2AuthenticationProvider();
218+
}
219+
220+
@Configuration
221+
public static class WebServerConfig {
222+
@RestController
223+
public class SecuredController {
224+
@GetMapping("/")
225+
public String ok() {
226+
return "OK";
227+
}
228+
}
229+
230+
@Bean
231+
MockWebServer server() {
232+
return new MockWebServer();
233+
}
234+
}
235+
236+
private MockHttpServletRequestBuilder bearer(MockHttpServletRequestBuilder mock, String authority) {
237+
return mock.header("Authorization", "Bearer " + authority);
238+
}
239+
240+
private void setupJwks(JwkSetBuilder builder) {
241+
MockResponse response = new MockResponse()
242+
.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
243+
.setBody(builder.build());
244+
245+
this.server.enqueue(response);
246+
this.server.enqueue(response);
247+
248+
}
249+
250+
private void register(Class<?>... classes) {
251+
this.context = new AnnotationConfigWebApplicationContext();
252+
this.context.register(classes);
253+
this.context.setServletContext(new MockServletContext());
254+
this.context.setServletConfig(new MockServletConfig());
255+
this.context.refresh();
256+
this.mvc = MockMvcBuilders.webAppContextSetup(this.context)
257+
.apply(springSecurity()).build();
258+
259+
this.server = this.context.getBean(MockWebServer.class);
260+
}
261+
}

0 commit comments

Comments
 (0)