Skip to content

Commit f755580

Browse files
committed
Resource Server Static Key Sample
Fixes: gh-5486
1 parent 090000c commit f755580

File tree

6 files changed

+328
-0
lines changed

6 files changed

+328
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
= OAuth 2.0 Resource Server Sample
2+
3+
This sample demonstrates integrating Resource Server with a pre-configured key.
4+
5+
With it, you can run the integration tests or run the application as a stand-alone service to explore how you can
6+
secure your own service with OAuth 2.0 Bearer Tokens using Spring Security.
7+
8+
== 1. Running the tests
9+
10+
To run the tests, do:
11+
12+
```bash
13+
./gradlew integrationTest
14+
```
15+
16+
Or import the project into your IDE and run `OAuth2ResourceServerApplicationITests` from there.
17+
18+
=== What is it doing?
19+
20+
By default, the application is configured with an RSA public key that is available in the sample.
21+
22+
The tests are configured with a set of hard-coded tokens that are signed with the corresponding RSA private key.
23+
Each test makes a query to the Resource Server with their corresponding token.
24+
25+
The Resource Server subsequently verifies the token against the public key and authorizes the request, returning the phrase
26+
27+
```bash
28+
Hello, subject!
29+
```
30+
31+
where "subject" is the value of the `sub` field in the token.
32+
33+
== 2. Running the app
34+
35+
To run as a stand-alone application, do:
36+
37+
```bash
38+
./gradlew bootRun
39+
```
40+
41+
Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there.
42+
43+
Once it is up, you can use the following token:
44+
45+
```bash
46+
export TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyfQ.eB2c9xtg5wcCZxZ-o-sH4Mx1JGkqAZwH4_WS0UcDbj_nen0NPBj6CqOEPhr_LZDagb4mM6HoAPJywWWG8b_Ylnn5r2gWDzib2mb0kxIuAjnvVBrpzusw4ItTVvP_srv2DrwcisKYiKqU5X_3ka7MSVvKtswdLY3RXeCJ_S2W9go
47+
```
48+
49+
And then make this request:
50+
51+
```bash
52+
curl -H "Authorization: Bearer $TOKEN" localhost:8080
53+
```
54+
55+
Which will respond with the phrase:
56+
57+
```bash
58+
Hello, subject!
59+
```
60+
61+
where `subject` is the value of the `sub` field in the token.
62+
63+
Or this:
64+
65+
```bash
66+
export TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCJ9.bsRCpUEaiWnzX4OqNxTBqwUD4vxxtPp-CHKTw7XcrglrvZ2lvYXaiZZbCp-hcPhuzMEzEAFuH6s4GZZOWVIX-wT47GdTz9cfA-Z4QPjS2RxePKphFXgBI3jHEpQo94Qya2fJdV4LvgBmA1uM_RTnYY1UbmeYuHKnXrZoGyV8QQQ
67+
68+
curl -H "Authorization: Bearer $TOKEN" localhost:8080/message
69+
```
70+
71+
Will respond with:
72+
73+
```bash
74+
secret message
75+
```
76+
77+
== 3. Testing with Other Tokens
78+
79+
You can create your own tokens. Simply edit the public key in `OAuth2ResourceServerSecurityConfiguration` to match the private key you use.
80+
81+
To use the `/` endpoint, any valid token will do.
82+
To use the `/message` endpoint, the token should have the `message:read` scope.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apply plugin: 'io.spring.convention.spring-sample-boot'
2+
3+
dependencies {
4+
compile project(':spring-security-config')
5+
compile project(':spring-security-oauth2-jose')
6+
compile project(':spring-security-oauth2-resource-server')
7+
8+
compile 'org.springframework.boot:spring-boot-starter-web'
9+
10+
testCompile project(':spring-security-test')
11+
testCompile 'org.springframework.boot:spring-boot-starter-test'
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2002-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 sample;
17+
18+
import org.junit.Test;
19+
import org.junit.runner.RunWith;
20+
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
23+
import org.springframework.boot.test.context.SpringBootTest;
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.mock.web.MockHttpServletRequest;
26+
import org.springframework.test.context.ActiveProfiles;
27+
import org.springframework.test.context.junit4.SpringRunner;
28+
import org.springframework.test.web.servlet.MockMvc;
29+
import org.springframework.test.web.servlet.request.RequestPostProcessor;
30+
31+
import static org.hamcrest.Matchers.containsString;
32+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
33+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
34+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
35+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
36+
37+
/**
38+
* Integration tests for {@link OAuth2ResourceServerApplication}
39+
*
40+
* @author Josh Cummings
41+
*/
42+
@RunWith(SpringRunner.class)
43+
@SpringBootTest
44+
@AutoConfigureMockMvc
45+
@ActiveProfiles("test")
46+
public class OAuth2ResourceServerApplicationITests {
47+
48+
String noScopesToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyfQ.eB2c9xtg5wcCZxZ-o-sH4Mx1JGkqAZwH4_WS0UcDbj_nen0NPBj6CqOEPhr_LZDagb4mM6HoAPJywWWG8b_Ylnn5r2gWDzib2mb0kxIuAjnvVBrpzusw4ItTVvP_srv2DrwcisKYiKqU5X_3ka7MSVvKtswdLY3RXeCJ_S2W9go";
49+
String messageReadToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCJ9.bsRCpUEaiWnzX4OqNxTBqwUD4vxxtPp-CHKTw7XcrglrvZ2lvYXaiZZbCp-hcPhuzMEzEAFuH6s4GZZOWVIX-wT47GdTz9cfA-Z4QPjS2RxePKphFXgBI3jHEpQo94Qya2fJdV4LvgBmA1uM_RTnYY1UbmeYuHKnXrZoGyV8QQQ";
50+
51+
@Autowired
52+
MockMvc mvc;
53+
54+
@Test
55+
public void performWhenValidBearerTokenThenAllows()
56+
throws Exception {
57+
58+
this.mvc.perform(get("/").with(bearerToken(this.noScopesToken)))
59+
.andExpect(status().isOk())
60+
.andExpect(content().string(containsString("Hello, subject!")));
61+
}
62+
63+
// -- tests with scopes
64+
65+
@Test
66+
public void performWhenValidBearerTokenThenScopedRequestsAlsoWork()
67+
throws Exception {
68+
69+
this.mvc.perform(get("/message").with(bearerToken(this.messageReadToken)))
70+
.andExpect(status().isOk())
71+
.andExpect(content().string(containsString("secret message")));
72+
}
73+
74+
@Test
75+
public void performWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess()
76+
throws Exception {
77+
78+
this.mvc.perform(get("/message").with(bearerToken(this.noScopesToken)))
79+
.andExpect(status().isForbidden())
80+
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
81+
containsString("Bearer error=\"insufficient_scope\"")));
82+
}
83+
84+
private static class BearerTokenRequestPostProcessor implements RequestPostProcessor {
85+
private String token;
86+
87+
public BearerTokenRequestPostProcessor(String token) {
88+
this.token = token;
89+
}
90+
91+
@Override
92+
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
93+
request.addHeader("Authorization", "Bearer " + this.token);
94+
return request;
95+
}
96+
}
97+
98+
private static BearerTokenRequestPostProcessor bearerToken(String token) {
99+
return new BearerTokenRequestPostProcessor(token);
100+
}
101+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 sample;
17+
18+
import org.springframework.boot.SpringApplication;
19+
import org.springframework.boot.autoconfigure.SpringBootApplication;
20+
21+
/**
22+
* @author Josh Cummings
23+
*/
24+
@SpringBootApplication
25+
public class OAuth2ResourceServerApplication {
26+
27+
public static void main(String[] args) {
28+
SpringApplication.run(OAuth2ResourceServerApplication.class, args);
29+
}
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 sample;
17+
18+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
19+
import org.springframework.security.oauth2.jwt.Jwt;
20+
import org.springframework.web.bind.annotation.GetMapping;
21+
import org.springframework.web.bind.annotation.RestController;
22+
23+
/**
24+
* @author Josh Cummings
25+
*/
26+
@RestController
27+
public class OAuth2ResourceServerController {
28+
29+
@GetMapping("/")
30+
public String index(@AuthenticationPrincipal Jwt jwt) {
31+
return String.format("Hello, %s!", jwt.getSubject());
32+
}
33+
34+
@GetMapping("/message")
35+
public String message() {
36+
return "secret message";
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 sample;
17+
18+
import java.security.KeyFactory;
19+
import java.security.interfaces.RSAPublicKey;
20+
import java.security.spec.X509EncodedKeySpec;
21+
import java.util.Base64;
22+
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
25+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
26+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
27+
import org.springframework.security.oauth2.jwt.JwtDecoder;
28+
import org.springframework.security.oauth2.jwt.JwtProcessors;
29+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
30+
31+
/**
32+
* @author Josh Cummings
33+
*/
34+
@EnableWebSecurity
35+
public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
36+
37+
@Override
38+
protected void configure(HttpSecurity http) throws Exception {
39+
// @formatter:off
40+
http
41+
.authorizeRequests()
42+
.antMatchers("/message/**").hasAuthority("SCOPE_message:read")
43+
.anyRequest().authenticated()
44+
.and()
45+
.oauth2ResourceServer()
46+
.jwt()
47+
.decoder(jwtDecoder());
48+
// @formatter:on
49+
}
50+
51+
@Bean
52+
JwtDecoder jwtDecoder() throws Exception {
53+
return new NimbusJwtDecoder(JwtProcessors.withPublicKey(key()).build());
54+
}
55+
56+
private RSAPublicKey key() throws Exception {
57+
String encoded = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd" +
58+
"UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs" +
59+
"HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D" +
60+
"o2kQ+X5xK9cipRgEKwIDAQAB";
61+
byte[] bytes = Base64.getDecoder().decode(encoded.getBytes());
62+
return (RSAPublicKey) KeyFactory.getInstance("RSA")
63+
.generatePublic(new X509EncodedKeySpec(bytes));
64+
}
65+
}

0 commit comments

Comments
 (0)