Skip to content

Commit 4fe8d3b

Browse files
jbellmannjgrandja
authored andcommitted
Add JwkSet endpoint as filter
Fixes spring-projectsgh-2
1 parent 3cdf4b5 commit 4fe8d3b

7 files changed

+285
-7
lines changed

build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ group = 'org.springframework.security.experimental'
1616
description = 'Spring Authorization Server'
1717
version = '0.0.1-SNAPSHOT'
1818

19+
ext['junit-jupiter.version'] = '5.4.0'
20+
1921
repositories {
2022
mavenCentral()
2123
}

samples/boot/minimal/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Minimal Authorization Server Sample
2+
3+
#### How to run
4+
5+
```
6+
./gradlew spring-authorization-server-samples-boot-minimal:bootRun
7+
```
8+
9+
```
10+
curl http://localhost:8080/.well-known/jwk_uris
11+
```
12+

samples/boot/minimal/spring-authorization-server-samples-boot-minimal.gradle

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
apply plugin: 'io.spring.convention.spring-sample-boot'
22

33
dependencies {
4-
implementation 'org.springframework.boot:spring-boot-starter'
4+
implementation 'org.springframework.boot:spring-boot-starter-web'
5+
implementation 'org.springframework.boot:spring-boot-starter-security'
6+
7+
implementation 'com.nimbusds:oauth2-oidc-sdk'
8+
59
testImplementation('org.springframework.boot:spring-boot-starter-test') {
610
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
711
}
12+
13+
testImplementation 'org.springframework.security:spring-security-test'
14+
15+
testRuntime("org.junit.platform:junit-platform-runner")
16+
testRuntime("org.junit.jupiter:junit-jupiter-engine")
817
}
918

1019
test {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2020 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+
* https://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+
17+
package sample;
18+
19+
import static org.springframework.http.HttpMethod.GET;
20+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
21+
22+
import java.io.IOException;
23+
import java.io.Writer;
24+
25+
import javax.servlet.FilterChain;
26+
import javax.servlet.ServletException;
27+
import javax.servlet.http.HttpServletRequest;
28+
import javax.servlet.http.HttpServletResponse;
29+
30+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
31+
import org.springframework.security.web.util.matcher.RequestMatcher;
32+
import org.springframework.util.Assert;
33+
import org.springframework.web.filter.OncePerRequestFilter;
34+
import org.springframework.web.util.UrlPathHelper;
35+
36+
import com.nimbusds.jose.jwk.JWKSet;
37+
38+
public class JwkSetEndpointFilter extends OncePerRequestFilter {
39+
40+
static final String WELL_KNOWN_JWK_URIS = "/.well-known/jwk_uris";
41+
42+
private final RequestMatcher requestMatcher = new AntPathRequestMatcher(WELL_KNOWN_JWK_URIS, GET.name(), true,
43+
new UrlPathHelper());
44+
45+
private final JWKSet jwkSet;
46+
47+
public JwkSetEndpointFilter(JWKSet jwkSet) {
48+
Assert.notNull(jwkSet, "jwkSet cannot be null");
49+
this.jwkSet = jwkSet;
50+
}
51+
52+
@Override
53+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
54+
throws ServletException, IOException {
55+
56+
if (ifRequestMatches(request)) {
57+
respond(response);
58+
} else {
59+
filterChain.doFilter(request, response);
60+
}
61+
}
62+
63+
private void respond(HttpServletResponse response) throws IOException {
64+
response.setContentType(APPLICATION_JSON_VALUE);
65+
try (Writer writer = response.getWriter()) {
66+
writer.write(jwkSet.toPublicJWKSet().toJSONObject().toJSONString());
67+
}
68+
}
69+
70+
private boolean ifRequestMatches(HttpServletRequest request) {
71+
return this.requestMatcher.matches(request);
72+
}
73+
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2020 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+
* https://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+
17+
package sample;
18+
19+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
20+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
21+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
22+
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
23+
24+
import com.nimbusds.jose.JOSEException;
25+
import com.nimbusds.jose.jwk.JWK;
26+
import com.nimbusds.jose.jwk.JWKSet;
27+
import com.nimbusds.jose.jwk.KeyUse;
28+
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
29+
30+
@EnableWebSecurity
31+
public class SecurityConfig extends WebSecurityConfigurerAdapter {
32+
33+
@Override
34+
protected void configure(HttpSecurity http) throws Exception {
35+
http.addFilterBefore(new JwkSetEndpointFilter(generateJwkSet()), ChannelProcessingFilter.class);
36+
}
37+
38+
protected JWKSet generateJwkSet() throws JOSEException {
39+
JWK jwk = new RSAKeyGenerator(2048).keyID("minimal-ASA").keyUse(KeyUse.SIGNATURE).generate();
40+
return new JWKSet(jwk);
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2020 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+
* https://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+
17+
package sample;
18+
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
import static org.hamcrest.Matchers.is;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.never;
23+
import static org.mockito.Mockito.only;
24+
import static org.mockito.Mockito.verify;
25+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
26+
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
27+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
28+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
29+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
30+
import static sample.JwkSetEndpointFilter.WELL_KNOWN_JWK_URIS;
31+
32+
import javax.servlet.FilterChain;
33+
import javax.servlet.http.HttpServletRequest;
34+
import javax.servlet.http.HttpServletResponse;
35+
36+
import org.junit.jupiter.api.BeforeAll;
37+
import org.junit.jupiter.api.Test;
38+
import org.junit.jupiter.api.TestInstance;
39+
import org.junit.jupiter.api.TestInstance.Lifecycle;
40+
import org.mockito.Mockito;
41+
import org.springframework.mock.web.MockHttpServletRequest;
42+
import org.springframework.mock.web.MockHttpServletResponse;
43+
import org.springframework.test.web.servlet.MockMvc;
44+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
45+
import org.springframework.web.bind.annotation.RequestMapping;
46+
import org.springframework.web.bind.annotation.RestController;
47+
48+
import com.nimbusds.jose.JOSEException;
49+
import com.nimbusds.jose.jwk.JWK;
50+
import com.nimbusds.jose.jwk.JWKSet;
51+
import com.nimbusds.jose.jwk.KeyUse;
52+
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
53+
54+
@TestInstance(Lifecycle.PER_CLASS)
55+
public class JwkSetEndpointFilterTest {
56+
57+
private MockMvc mvc;
58+
private JWKSet jwkSet;
59+
private JWK jwk;
60+
private JwkSetEndpointFilter filter;
61+
62+
@BeforeAll
63+
void setup() throws JOSEException {
64+
this.jwk = new RSAKeyGenerator(2048).keyID("endpoint-test").keyUse(KeyUse.SIGNATURE).generate();
65+
this.jwkSet = new JWKSet(jwk);
66+
this.filter = new JwkSetEndpointFilter(jwkSet);
67+
this.mvc = MockMvcBuilders.standaloneSetup(new FakeController()).addFilters(filter).alwaysDo(print()).build();
68+
}
69+
70+
@Test
71+
void constructorWhenJsonWebKeySetIsNullThrowIllegalArgumentException() {
72+
assertThatThrownBy(() -> new JwkSetEndpointFilter(null)).isInstanceOf(IllegalArgumentException.class);
73+
}
74+
75+
@Test
76+
void doFilterWhenPathMatches() throws Exception {
77+
String requestUri = WELL_KNOWN_JWK_URIS;
78+
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
79+
request.setServletPath(requestUri);
80+
81+
MockHttpServletResponse response = new MockHttpServletResponse();
82+
FilterChain filterChain = mock(FilterChain.class);
83+
84+
this.filter.doFilter(request, response, filterChain);
85+
86+
verify(filterChain, never()).doFilter(Mockito.any(HttpServletRequest.class),
87+
Mockito.any(HttpServletResponse.class));
88+
}
89+
90+
@Test
91+
void doFilterWhenPathDoesNotMatch() throws Exception {
92+
String requestUri = "/stuff/" + WELL_KNOWN_JWK_URIS;
93+
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
94+
request.setServletPath(requestUri);
95+
96+
MockHttpServletResponse response = new MockHttpServletResponse();
97+
FilterChain filterChain = mock(FilterChain.class);
98+
99+
this.filter.doFilter(request, response, filterChain);
100+
101+
verify(filterChain, only()).doFilter(Mockito.any(HttpServletRequest.class),
102+
Mockito.any(HttpServletResponse.class));
103+
}
104+
105+
@Test
106+
void testResponseIfRequestMatches() throws Exception {
107+
mvc.perform(get(WELL_KNOWN_JWK_URIS)).andDo(print()).andExpect(status().isOk())
108+
.andExpect(jsonPath("$.keys").isArray()).andExpect(jsonPath("$.keys").isNotEmpty())
109+
.andExpect(jsonPath("$.keys[0].kid").value(jwk.getKeyID()))
110+
.andExpect(jsonPath("$.keys[0].kty").value(jwk.getKeyType().toString()));
111+
}
112+
113+
@Test
114+
void testResponseIfNotRequestMatches() throws Exception {
115+
mvc.perform(get("/fake")).andDo(print()).andExpect(status().isOk())
116+
.andExpect(content().string(is("fake")));
117+
}
118+
119+
@RestController
120+
class FakeController {
121+
122+
@RequestMapping("/fake")
123+
public String hello() {
124+
return "fake";
125+
}
126+
}
127+
}

samples/boot/minimal/src/test/java/sample/MinimalAuthorizationServerApplicationTests.java

+17-6
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,29 @@
1515
*/
1616
package sample;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.springframework.http.HttpStatus.OK;
20+
1821
import org.junit.jupiter.api.Test;
1922
import org.springframework.boot.test.context.SpringBootTest;
20-
import org.springframework.context.ApplicationContext;
21-
22-
import static org.assertj.core.api.Assertions.assertThat;
23+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
24+
import org.springframework.boot.web.server.LocalServerPort;
25+
import org.springframework.http.ResponseEntity;
26+
import org.springframework.web.client.RestTemplate;
2327

24-
@SpringBootTest
28+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
2529
public class MinimalAuthorizationServerApplicationTests {
2630

31+
private RestTemplate rest = new RestTemplate();
32+
33+
@LocalServerPort
34+
private int serverPort;
35+
2736
@Test
28-
public void loadContext(ApplicationContext context) {
29-
assertThat(context).isNotNull();
37+
void verifyJwkSetEndpointFilterAccessibleWithoutAuthentication() {
38+
ResponseEntity<String> responseEntity = rest.getForEntity(
39+
"http://localhost:" + serverPort + JwkSetEndpointFilter.WELL_KNOWN_JWK_URIS, String.class);
40+
assertThat(responseEntity.getStatusCode()).isEqualTo(OK);
3041
}
3142

3243
}

0 commit comments

Comments
 (0)