소스 검색

Resource Server Static Key Sample

Fixes: gh-5486
Josh Cummings 6 년 전
부모
커밋
f755580a91

+ 82 - 0
samples/boot/oauth2resourceserver-static/README.adoc

@@ -0,0 +1,82 @@
+= OAuth 2.0 Resource Server Sample
+
+This sample demonstrates integrating Resource Server with a pre-configured key.
+
+With it, you can run the integration tests or run the application as a stand-alone service to explore how you can
+secure your own service with OAuth 2.0 Bearer Tokens using Spring Security.
+
+== 1. Running the tests
+
+To run the tests, do:
+
+```bash
+./gradlew integrationTest
+```
+
+Or import the project into your IDE and run `OAuth2ResourceServerApplicationITests` from there.
+
+=== What is it doing?
+
+By default, the application is configured with an RSA public key that is available in the sample.
+
+The tests are configured with a set of hard-coded tokens that are signed with the corresponding RSA private key.
+Each test makes a query to the Resource Server with their corresponding token.
+
+The Resource Server subsequently verifies the token against the public key and authorizes the request, returning the phrase
+
+```bash
+Hello, subject!
+```
+
+where "subject" is the value of the `sub` field in the token.
+
+== 2. Running the app
+
+To run as a stand-alone application, do:
+
+```bash
+./gradlew bootRun
+```
+
+Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there.
+
+Once it is up, you can use the following token:
+
+```bash
+export TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyfQ.eB2c9xtg5wcCZxZ-o-sH4Mx1JGkqAZwH4_WS0UcDbj_nen0NPBj6CqOEPhr_LZDagb4mM6HoAPJywWWG8b_Ylnn5r2gWDzib2mb0kxIuAjnvVBrpzusw4ItTVvP_srv2DrwcisKYiKqU5X_3ka7MSVvKtswdLY3RXeCJ_S2W9go
+```
+
+And then make this request:
+
+```bash
+curl -H "Authorization: Bearer $TOKEN" localhost:8080
+```
+
+Which will respond with the phrase:
+
+```bash
+Hello, subject!
+```
+
+where `subject` is the value of the `sub` field in the token.
+
+Or this:
+
+```bash
+export TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCJ9.bsRCpUEaiWnzX4OqNxTBqwUD4vxxtPp-CHKTw7XcrglrvZ2lvYXaiZZbCp-hcPhuzMEzEAFuH6s4GZZOWVIX-wT47GdTz9cfA-Z4QPjS2RxePKphFXgBI3jHEpQo94Qya2fJdV4LvgBmA1uM_RTnYY1UbmeYuHKnXrZoGyV8QQQ
+
+curl -H "Authorization: Bearer $TOKEN" localhost:8080/message
+```
+
+Will respond with:
+
+```bash
+secret message
+```
+
+== 3. Testing with Other Tokens
+
+You can create your own tokens. Simply edit the public key in `OAuth2ResourceServerSecurityConfiguration` to match the private key you use.
+
+To use the `/` endpoint, any valid token will do.
+To use the `/message` endpoint, the token should have the `message:read` scope.

+ 12 - 0
samples/boot/oauth2resourceserver-static/spring-security-samples-boot-oauth2resourceserver-static.gradle

@@ -0,0 +1,12 @@
+apply plugin: 'io.spring.convention.spring-sample-boot'
+
+dependencies {
+	compile project(':spring-security-config')
+	compile project(':spring-security-oauth2-jose')
+	compile project(':spring-security-oauth2-resource-server')
+
+	compile 'org.springframework.boot:spring-boot-starter-web'
+
+	testCompile project(':spring-security-test')
+	testCompile 'org.springframework.boot:spring-boot-starter-test'
+}

+ 101 - 0
samples/boot/oauth2resourceserver-static/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java

@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Integration tests for {@link OAuth2ResourceServerApplication}
+ *
+ * @author Josh Cummings
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+public class OAuth2ResourceServerApplicationITests {
+
+	String noScopesToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyfQ.eB2c9xtg5wcCZxZ-o-sH4Mx1JGkqAZwH4_WS0UcDbj_nen0NPBj6CqOEPhr_LZDagb4mM6HoAPJywWWG8b_Ylnn5r2gWDzib2mb0kxIuAjnvVBrpzusw4ItTVvP_srv2DrwcisKYiKqU5X_3ka7MSVvKtswdLY3RXeCJ_S2W9go";
+	String messageReadToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCJ9.bsRCpUEaiWnzX4OqNxTBqwUD4vxxtPp-CHKTw7XcrglrvZ2lvYXaiZZbCp-hcPhuzMEzEAFuH6s4GZZOWVIX-wT47GdTz9cfA-Z4QPjS2RxePKphFXgBI3jHEpQo94Qya2fJdV4LvgBmA1uM_RTnYY1UbmeYuHKnXrZoGyV8QQQ";
+
+	@Autowired
+	MockMvc mvc;
+
+	@Test
+	public void performWhenValidBearerTokenThenAllows()
+		throws Exception {
+
+		this.mvc.perform(get("/").with(bearerToken(this.noScopesToken)))
+				.andExpect(status().isOk())
+				.andExpect(content().string(containsString("Hello, subject!")));
+	}
+
+	// -- tests with scopes
+
+	@Test
+	public void performWhenValidBearerTokenThenScopedRequestsAlsoWork()
+			throws Exception {
+
+		this.mvc.perform(get("/message").with(bearerToken(this.messageReadToken)))
+				.andExpect(status().isOk())
+				.andExpect(content().string(containsString("secret message")));
+	}
+
+	@Test
+	public void performWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess()
+			throws Exception {
+
+		this.mvc.perform(get("/message").with(bearerToken(this.noScopesToken)))
+				.andExpect(status().isForbidden())
+				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE,
+						containsString("Bearer error=\"insufficient_scope\"")));
+	}
+
+	private static class BearerTokenRequestPostProcessor implements RequestPostProcessor {
+		private String token;
+
+		public BearerTokenRequestPostProcessor(String token) {
+			this.token = token;
+		}
+
+		@Override
+		public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
+			request.addHeader("Authorization", "Bearer " + this.token);
+			return request;
+		}
+	}
+
+	private static BearerTokenRequestPostProcessor bearerToken(String token) {
+		return new BearerTokenRequestPostProcessor(token);
+	}
+}

+ 30 - 0
samples/boot/oauth2resourceserver-static/src/main/java/sample/OAuth2ResourceServerApplication.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Josh Cummings
+ */
+@SpringBootApplication
+public class OAuth2ResourceServerApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(OAuth2ResourceServerApplication.class, args);
+	}
+}

+ 38 - 0
samples/boot/oauth2resourceserver-static/src/main/java/sample/OAuth2ResourceServerController.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Josh Cummings
+ */
+@RestController
+public class OAuth2ResourceServerController {
+
+	@GetMapping("/")
+	public String index(@AuthenticationPrincipal Jwt jwt) {
+		return String.format("Hello, %s!", jwt.getSubject());
+	}
+
+	@GetMapping("/message")
+	public String message() {
+		return "secret message";
+	}
+}

+ 65 - 0
samples/boot/oauth2resourceserver-static/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import java.security.KeyFactory;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtProcessors;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+
+/**
+ * @author Josh Cummings
+ */
+@EnableWebSecurity
+public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+	@Override
+	protected void configure(HttpSecurity http) throws Exception {
+		// @formatter:off
+		http
+			.authorizeRequests()
+				.antMatchers("/message/**").hasAuthority("SCOPE_message:read")
+				.anyRequest().authenticated()
+				.and()
+			.oauth2ResourceServer()
+				.jwt()
+					.decoder(jwtDecoder());
+		// @formatter:on
+	}
+
+	@Bean
+	JwtDecoder jwtDecoder() throws Exception {
+		return new NimbusJwtDecoder(JwtProcessors.withPublicKey(key()).build());
+	}
+
+	private RSAPublicKey key() throws Exception {
+		String encoded = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd" +
+				"UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs" +
+				"HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D" +
+				"o2kQ+X5xK9cipRgEKwIDAQAB";
+		byte[] bytes = Base64.getDecoder().decode(encoded.getBytes());
+		return (RSAPublicKey) KeyFactory.getInstance("RSA")
+				.generatePublic(new X509EncodedKeySpec(bytes));
+	}
+}