Browse Source

Polish resourceserver samples

- Use ${mockserver.url} instead of mock://
- Consistency between reactive/imperative samples

Fixes: gh-5844
Rob Winch 7 years ago
parent
commit
c21b2f31c6
19 changed files with 470 additions and 295 deletions
  1. 44 101
      samples/boot/oauth2resourceserver-webflux/README.adoc
  2. 1 0
      samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle
  3. 79 0
      samples/boot/oauth2resourceserver-webflux/src/integration-test/java/sample/ServerOAuth2ResourceServerApplicationITests.java
  4. 41 0
      samples/boot/oauth2resourceserver-webflux/src/main/java/org/springframework/boot/env/MockWebServerEnvironmentPostProcessor.java
  5. 63 57
      samples/boot/oauth2resourceserver-webflux/src/main/java/org/springframework/boot/env/MockWebServerPropertySource.java
  6. 22 0
      samples/boot/oauth2resourceserver-webflux/src/main/java/org/springframework/boot/env/package-info.java
  7. 5 0
      samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java
  8. 3 37
      samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java
  9. 1 0
      samples/boot/oauth2resourceserver-webflux/src/main/resources/META-INF/spring.factories
  10. 6 11
      samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml
  11. 0 76
      samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java
  12. 12 4
      samples/boot/oauth2resourceserver/README.adoc
  13. 0 1
      samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml
  14. 41 0
      samples/boot/oauth2resourceserver/src/main/java/org/springframework/boot/env/MockWebServerEnvironmentPostProcessor.java
  15. 121 0
      samples/boot/oauth2resourceserver/src/main/java/org/springframework/boot/env/MockWebServerPropertySource.java
  16. 22 0
      samples/boot/oauth2resourceserver/src/main/java/org/springframework/boot/env/package-info.java
  17. 2 6
      samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java
  18. 1 1
      samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories
  19. 6 1
      samples/boot/oauth2resourceserver/src/main/resources/application.yml

+ 44 - 101
samples/boot/oauth2resourceserver-webflux/README.adoc

@@ -1,6 +1,7 @@
 = OAuth 2.0 Resource Server Sample
 = OAuth 2.0 Resource Server Sample
 
 
-This sample demonstrates integrations with a handful of different authorization servers.
+This sample demonstrates integrating Resource Server with a mock Authorization Server, though it can be modified to integrate
+with your favorite Authorization Server.
 
 
 With it, you can run the integration tests or run the application as a stand-alone service to explore how you can
 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.
 secure your own service with OAuth 2.0 Bearer Tokens using Spring Security.
@@ -10,160 +11,102 @@ secure your own service with OAuth 2.0 Bearer Tokens using Spring Security.
 To run the tests, do:
 To run the tests, do:
 
 
 ```bash
 ```bash
-../../../gradlew integrationTest
+./gradlew integrationTest
 ```
 ```
 
 
-Or import the project into your IDE and run `OAuth2ResourceServerApplicationTests` from there.
+Or import the project into your IDE and run `ServerOAuth2ResourceServerApplicationTests` from there.
 
 
 === What is it doing?
 === What is it doing?
 
 
-By default, the tests are pointing at a demonstration Okta instance. The test that performs a valid round trip does so
-by querying the Okta Authorization Server using the client_credentials grant type to get a valid JWT token. Then, the test
-makes a query to the Resource Server with that token. The Resource Server subsquently verifies with Okta and
-authorizes the request, returning the phrase
+By default, the tests are pointing at a mock Authorization Server instance.
+
+The tests are configured with a set of hard-coded tokens originally obtained from the mock Authorization Server,
+and each makes a query to the Resource Server with their corresponding token.
+
+The Resource Server subsquently verifies with the Authorization Server and authorizes the request, returning the phrase
 
 
 ```bash
 ```bash
-Hello, {subject}!
+Hello, subject!
 ```
 ```
 
 
-where subject is the value of the `sub` field in the JWT returned by the Authorization Server.
+where "subject" is the value of the `sub` field in the JWT returned by the Authorization Server.
 
 
 == 2. Running the app
 == 2. Running the app
 
 
 To run as a stand-alone application, do:
 To run as a stand-alone application, do:
 
 
 ```bash
 ```bash
-../../../gradlew bootRun
+./gradlew bootRun
 ```
 ```
 
 
-Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there.
+Or import the project into your IDE and run `ServerOAuth2ResourceServerApplication` from there.
 
 
-Once it is up, you can retreive a valid JWT token from the authorization server, and then hit the endpoint:
+Once it is up, you can use the following token:
 
 
 ```bash
 ```bash
-curl -H "Authorization: Bearer {token}" localhost:8081
+export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww
 ```
 ```
 
 
-Which will respond with the phrase:
+And then make this request:
 
 
 ```bash
 ```bash
-Hello, {subject}!
+curl -H "Authorization: Bearer $TOKEN" localhost:8080
 ```
 ```
 
 
-where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server.
-
-=== How do I obtain a valid JWT token?
-
-Getting a valid JWT token from an Authorization Server will vary, depending on your setup. However, it will typically
-look something like this:
+Which will respond with the phrase:
 
 
 ```bash
 ```bash
-curl --user {client id}:{client password} -d "grant_type=client_credentials" {auth server endpoint}/token
+Hello, subject!
 ```
 ```
 
 
-which will respond with a JSON payload containing the `access_token` among other things:
-
-```bash
-{ "access_token" : "{the access token}", "token_type" : "Bearer", "expires_in" : "{an expiry}", "scope" : "{a list of scopes}" }
-```
+where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server.
 
 
-For example, the following can be used to hit the sample Okta endpoint for a valid JWT token:
+Or this:
 
 
 ```bash
 ```bash
-curl --user 0oaf5u5g4m6CW4x6z0h7:HR7edRoo3glhF06HTxonOKZvO4I2BWYcC_ocOHlv -d "grant_type=client_credentials" https://dev-805262.oktapreview.com/oauth2/default/v1/token
-```
-
-Which will give a response similar to this (formatting mine):
+export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A
 
 
-```json
-{
-  "access_token": "eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ",
-  "token_type": "Bearer",
-  "expires_in": 3600,
-  "scope": "ok"
-}
+curl -H "Authorization: Bearer $TOKEN" localhost:8080/message
 ```
 ```
 
 
-Then, using that access token:
+Will respond with:
 
 
 ```bash
 ```bash
-curl -H  "Authorization: Bearer eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ" \
-  localhost:8081
+secret message
 ```
 ```
 
 
-I get:
+== 2. Testing against other Authorization Servers
 
 
-```bash
-Hello, 0oaf5u5g4m6CW4x6z0h7!
-```
+_In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._
 
 
-== 3. Testing against other Authorization Servers
-
-The sample is already prepared to demonstrate integrations with a handful of other Authorization Servers. Do exercise
-one, simply uncomment two commented out sections, both in the application.yml file:
+To change the sample to point at your Authorization Server, simply find this property in the `application.yml`:
 
 
 ```yaml
 ```yaml
 spring:
 spring:
   security:
   security:
     oauth2:
     oauth2:
       resourceserver:
       resourceserver:
-        issuer:
+        jwt:
+          jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json
 ```
 ```
 
 
-First, find the above section in the application.yml. Beneath it, you will see sections for each Authorization Server
-already prepared with the one for Okta commented out:
+And change the property to your Authorization Server's JWK set endpoint:
 
 
 ```yaml
 ```yaml
-#          master: #keycloak
-#            issuer: http://localhost:8080/auth/realms/master
-#            jwk-set-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/certs
-          okta:
-            issuer: https://dev-805262.oktapreview.com/oauth2/default
-            jwk-set-uri: https://dev-805262.oktapreview.com/oauth2/default/v1/keys
-```
-
-Comment out the `okta` section and uncomment the desired section.
-
-Second, find the following section, which the sample needs in order to retreive a valid token from the Authorization
-Server:
-
-```yaml
-#  ### keycloak
-#  token-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/token
-#  token-body:
-#    grant_type: client_credentials
-#  client-id: service
-#  client-password: 9114712b-be55-4dab-b270-04734abda1c4
-#  container:
-#    config-file-name: keycloak.config
-#    docker-file-name: keycloak.docker
-  ### okta
-  token-uri: https://dev-805262.oktapreview.com/oauth2/default/v1/token
-  token-body:
-    grant_type: client_credentials
-  client-id: 0oaf5u5g4m6CW4x6z0h7
-  client-password: HR7edRoo3glhF06HTxonOKZvO4I2BWYcC_ocOHlv
-```
-
-Comment out the `okta` section and uncomment the desired section.
-
-=== How can I test with my own Authorization Server instance?
-
-To test with your own Okta or other Authorization Server instance, simply provide the following information:
-
-```yaml
-spring.security.oauth2.resourceserver.issuer.name.uri: the issuer uri
-spring.security.oauth2.resourceserver.issuer.name.jwk-set-uri: the jwk key uri
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys
 ```
 ```
 
 
-And indicate, using the sample.provider properties, how the sample should generate a valid JWT token:
+And then you can run the app the same as before:
 
 
-```yaml
-sample.provider.token-uri: the token endpoint
-sample.provider.token-body.grant_type: the grant to use
-sample.provider.token-body.another_property: another_value
-sample.provider.client-id: the client id
-sample.provider.client-password: the client password, only required for confidential clients
+```bash
+./gradlew bootRun
 ```
 ```
 
 
-You can provide values for any OAuth 2.0-compliant Authorization Server.
+Make sure to obtain valid tokens from your Authorization Server in order to play with the sample Resource Server.
+To use the `/` endpoint, any valid token from your Authorization Server will do.
+To use the `/message` endpoint, the token should have the `message:read` scope.

+ 1 - 0
samples/boot/oauth2resourceserver-webflux/spring-security-samples-boot-oauth2resourceserver-webflux.gradle

@@ -7,6 +7,7 @@ dependencies {
 	compile project(':spring-security-oauth2-resource-server')
 	compile project(':spring-security-oauth2-resource-server')
 
 
 	compile 'org.springframework.boot:spring-boot-starter-webflux'
 	compile 'org.springframework.boot:spring-boot-starter-webflux'
+	compile 'com.squareup.okhttp3:mockwebserver'
 
 
 	testCompile project(':spring-security-test')
 	testCompile project(':spring-security-test')
 	testCompile 'org.springframework.boot:spring-boot-starter-test'
 	testCompile 'org.springframework.boot:spring-boot-starter-test'

+ 79 - 0
samples/boot/oauth2resourceserver-webflux/src/integration-test/java/sample/ServerOAuth2ResourceServerApplicationITests.java

@@ -0,0 +1,79 @@
+/*
+ * 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.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import java.util.function.Consumer;
+
+import static org.hamcrest.Matchers.containsString;
+
+/**
+ * @author Rob Winch
+ * @since 5.1
+ */
+@SpringBootTest
+@AutoConfigureWebTestClient
+@RunWith(SpringJUnit4ClassRunner.class)
+public class ServerOAuth2ResourceServerApplicationITests {
+
+	Consumer<HttpHeaders> noScopesToken = http -> http.setBearerAuth("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww");
+	Consumer<HttpHeaders> messageReadToken = http -> http.setBearerAuth("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A");
+
+	@Autowired
+	private WebTestClient rest;
+
+
+	@Test
+	public void getWhenValidBearerTokenThenAllows() {
+
+		this.rest.get().uri("/")
+				.headers(this.noScopesToken)
+				.exchange()
+				.expectStatus().isOk()
+				.expectBody(String.class).isEqualTo("Hello, subject!");
+	}
+
+	// -- tests with scopes
+
+	@Test
+	public void getWhenValidBearerTokenThenScopedRequestsAlsoWork() {
+
+		this.rest.get().uri("/message")
+				.headers(this.messageReadToken)
+				.exchange()
+				.expectStatus().isOk()
+				.expectBody(String.class).isEqualTo("secret message");
+	}
+
+	@Test
+	public void getWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() {
+
+		this.rest.get().uri("/message")
+				.headers(this.noScopesToken)
+				.exchange()
+				.expectStatus().isForbidden()
+				.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, containsString("Bearer error=\"insufficient_scope\""));
+	}
+}

+ 41 - 0
samples/boot/oauth2resourceserver-webflux/src/main/java/org/springframework/boot/env/MockWebServerEnvironmentPostProcessor.java

@@ -0,0 +1,41 @@
+/*
+ * 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 org.springframework.boot.env;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.boot.SpringApplication;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * @author Rob Winch
+ */
+public class MockWebServerEnvironmentPostProcessor
+		implements EnvironmentPostProcessor, DisposableBean {
+
+	private final MockWebServerPropertySource propertySource = new MockWebServerPropertySource();
+
+	@Override
+	public void postProcessEnvironment(ConfigurableEnvironment environment,
+			SpringApplication application) {
+		environment.getPropertySources().addFirst(this.propertySource);
+	}
+
+	@Override
+	public void destroy() throws Exception {
+		this.propertySource.destroy();
+	}
+}

+ 63 - 57
samples/boot/oauth2resourceserver/src/main/java/sample/provider/MockProvider.java → samples/boot/oauth2resourceserver-webflux/src/main/java/org/springframework/boot/env/MockWebServerPropertySource.java

@@ -14,47 +14,26 @@
  * limitations under the License.
  * limitations under the License.
  */
  */
 
 
-package sample.provider;
-
-import java.io.IOException;
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-import javax.annotation.PreDestroy;
+package org.springframework.boot.env;
 
 
 import okhttp3.mockwebserver.Dispatcher;
 import okhttp3.mockwebserver.Dispatcher;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import okhttp3.mockwebserver.MockWebServer;
 import okhttp3.mockwebserver.RecordedRequest;
 import okhttp3.mockwebserver.RecordedRequest;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.env.EnvironmentPostProcessor;
-import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.core.env.PropertySource;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.MediaType;
 
 
+import java.io.IOException;
+
 /**
 /**
- * This is a miminal mock server that serves as a placeholder for a real Authorization Server (AS).
- *
- * For the sample to work, the AS used must support a JWK endpoint.
- *
- * For the integration tests to work, the AS used must be able to issue a token
- * with the following characteristics:
- *
- * - The token has the "message:read" scope
- * - The token has a "sub" of "subject"
- * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS.
- *
- * There is also a test that verifies insufficient scope. In that case, the token should have the following characteristics:
- *
- * - The token is missing the "message:read" scope
- * - The token is signed by a RS256 private key whose public key counterpart is served from the JWK endpoint of the AS.
- *
- * @author Josh Cummings
+ * @author Rob Winch
  */
  */
-public class MockProvider implements EnvironmentPostProcessor {
-	private MockWebServer server = new MockWebServer();
+public class MockWebServerPropertySource extends PropertySource<MockWebServer> implements
+		DisposableBean {
 
 
 	private static final MockResponse JWKS_RESPONSE = response(
 	private static final MockResponse JWKS_RESPONSE = response(
 			"{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMwDBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK48M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6oXZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQPDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkFk5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrnuYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHsV9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC58CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eqD9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}",
 			"{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMwDBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK48M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6oXZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQPDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkFk5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrnuYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHsV9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC58CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eqD9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}",
@@ -66,7 +45,52 @@ public class MockProvider implements EnvironmentPostProcessor {
 			404
 			404
 	);
 	);
 
 
-	public MockProvider() throws IOException {
+	/**
+	 * Name of the random {@link PropertySource}.
+	 */
+	public static final String MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME = "mockwebserver";
+
+	private static final String NAME = "mockwebserver.url";
+
+	private static final Log logger = LogFactory.getLog(MockWebServerPropertySource.class);
+
+	private boolean started;
+
+	public MockWebServerPropertySource() {
+		super(MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME, new MockWebServer());
+	}
+
+	@Override
+	public Object getProperty(String name) {
+		if (!name.equals(NAME)) {
+			return null;
+		}
+		if (logger.isTraceEnabled()) {
+			logger.trace("Looking up the url for '" + name + "'");
+		}
+		String url = getUrl();
+		return url;
+	}
+
+	@Override
+	public void destroy() throws Exception {
+		getSource().shutdown();
+	}
+
+	/**
+	 * Get's the URL (i.e. "http://localhost:123456")
+	 * @return
+	 */
+	private String getUrl() {
+		MockWebServer mockWebServer = getSource();
+		if (!this.started) {
+			intializeMockWebServer(mockWebServer);
+		}
+		String url = mockWebServer.url("").url().toExternalForm();
+		return url.substring(0, url.length() - 1);
+	}
+
+	private void intializeMockWebServer(MockWebServer mockWebServer) {
 		Dispatcher dispatcher = new Dispatcher() {
 		Dispatcher dispatcher = new Dispatcher() {
 			@Override
 			@Override
 			public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
 			public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
@@ -78,38 +102,20 @@ public class MockProvider implements EnvironmentPostProcessor {
 			}
 			}
 		};
 		};
 
 
-		this.server.setDispatcher(dispatcher);
-	}
-
-	@Override
-	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
-		String uri = environment.getProperty("sample.jwk-set-uri", "mock://localhost:0");
-
-		if (uri.startsWith("mock://")) {
-			try {
-				this.server.start(URI.create(uri).getPort());
-			} catch (IOException e) {
-				throw new IllegalStateException(e);
-			}
-
-			Map<String, Object> properties = new HashMap<>();
-			String url = this.server.url("/.well-known/jwks.json").toString();
-			properties.put("sample.jwk-set-uri", url);
-
-			MapPropertySource propertySource = new MapPropertySource("mock", properties);
-			environment.getPropertySources().addFirst(propertySource);
+		mockWebServer.setDispatcher(dispatcher);
+		try {
+			mockWebServer.start();
+			this.started = true;
+		} catch (IOException e) {
+			throw new RuntimeException("Could not start " + mockWebServer, e);
 		}
 		}
 	}
 	}
 
 
-	@PreDestroy
-	public void shutdown() throws IOException {
-		this.server.shutdown();
-	}
-
 	private static MockResponse response(String body, int status) {
 	private static MockResponse response(String body, int status) {
 		return new MockResponse()
 		return new MockResponse()
 				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
 				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
 				.setResponseCode(status)
 				.setResponseCode(status)
 				.setBody(body);
 				.setBody(body);
 	}
 	}
+
 }
 }

+ 22 - 0
samples/boot/oauth2resourceserver-webflux/src/main/java/org/springframework/boot/env/package-info.java

@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * This provides integration of a {@link okhttp3.mockwebserver.MockWebServer} and the
+ * {@link org.springframework.core.env.Environment}
+ * @author Rob Winch
+ */
+package org.springframework.boot.env;

+ 5 - 0
samples/boot/oauth2resourceserver-webflux/src/main/java/sample/OAuth2ResourceServerController.java

@@ -30,4 +30,9 @@ public class OAuth2ResourceServerController {
 	public String index(@AuthenticationPrincipal Jwt jwt) {
 	public String index(@AuthenticationPrincipal Jwt jwt) {
 		return String.format("Hello, %s!", jwt.getSubject());
 		return String.format("Hello, %s!", jwt.getSubject());
 	}
 	}
+
+	@GetMapping("/message")
+	public String message() {
+		return "secret message";
+	}
 }
 }

+ 3 - 37
samples/boot/oauth2resourceserver-webflux/src/main/java/sample/SecurityConfig.java

@@ -16,61 +16,27 @@
 
 
 package sample;
 package sample;
 
 
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 
 
-import java.math.BigInteger;
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.RSAPublicKey;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.RSAPublicKeySpec;
-
 /**
 /**
  * @author Rob Winch
  * @author Rob Winch
  * @since 5.1
  * @since 5.1
  */
  */
 @EnableWebFluxSecurity
 @EnableWebFluxSecurity
 public class SecurityConfig {
 public class SecurityConfig {
-	private static final String JWK_SET_URI_PROP = "sample.jwk-set-uri";
 
 
 	@Bean
 	@Bean
-	@ConditionalOnProperty(SecurityConfig.JWK_SET_URI_PROP)
-	SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, @Value("${sample.jwk-set-uri}") String jwkSetUri) throws Exception {
+	SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
 		http
 		http
 			.authorizeExchange()
 			.authorizeExchange()
+				.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
 				.anyExchange().authenticated()
 				.anyExchange().authenticated()
 				.and()
 				.and()
 			.oauth2ResourceServer()
 			.oauth2ResourceServer()
-				.jwt()
-					.jwkSetUri(jwkSetUri);
+				.jwt();
 		return http.build();
 		return http.build();
 	}
 	}
-
-	@Bean
-	@ConditionalOnProperty(matchIfMissing = true, name = SecurityConfig.JWK_SET_URI_PROP)
-	SecurityWebFilterChain springSecurityFilterChainWithJwkSetUri(ServerHttpSecurity http) throws Exception {
-		http
-			.authorizeExchange()
-				.anyExchange().authenticated()
-				.and()
-			.oauth2ResourceServer()
-				.jwt()
-					.publicKey(publicKey());
-		return http.build();
-	}
-
-	private RSAPublicKey publicKey()
-			throws NoSuchAlgorithmException, InvalidKeySpecException {
-		String modulus = "21301844740604653578042500449274548398885553541276518010855123403873267398204269788903348794459771698460057967144865511347818036788093430902099139850950702438493841101242291810362822203615900335437741117578551216365305797763072813300890517195382010982402736091906390325356368590938709762826676219814134995844721978269999358693499223506089799649124650473473086179730568497569430199548044603025675755473148289824338392487941265829853008714754732175256733090080910187256164496297277607612684421019218165083081805792835073696987599616469568512360535527950859101589894643349122454163864596223876828010734083744763850611111";
-		String exponent = "65537";
-
-		RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent));
-		KeyFactory factory = KeyFactory.getInstance("RSA");
-		return (RSAPublicKey) factory.generatePublic(spec);
-	}
 }
 }

+ 1 - 0
samples/boot/oauth2resourceserver-webflux/src/main/resources/META-INF/spring.factories

@@ -0,0 +1 @@
+org.springframework.boot.env.EnvironmentPostProcessor=org.springframework.boot.env.MockWebServerEnvironmentPostProcessor

+ 6 - 11
samples/boot/oauth2resourceserver-webflux/src/main/resources/application.yml

@@ -1,11 +1,6 @@
-logging:
-  level:
-    root: INFO
-    org.springframework.web: INFO
-    org.springframework.security: INFO
-#    org.springframework.boot.autoconfigure: DEBUG
-
-sample:
-# By default this sample uses a hard coded public key in SecurityConfig
-# To use a JWK Set URI, uncomment and change the value below
-#  jwk-set-uri: https://example.com/oauth2/default/v1/keys
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json

+ 0 - 76
samples/boot/oauth2resourceserver-webflux/src/test/java/sample/ServerOauth2ResourceApplicationTests.java

@@ -1,76 +0,0 @@
-/*
- * 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.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.http.HttpHeaders;
-import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
-import org.springframework.test.web.reactive.server.WebTestClient;
-
-/**
- * @author Rob Winch
- * @since 5.1
- */
-@SpringBootTest
-@AutoConfigureWebTestClient
-@RunWith(SpringJUnit4ClassRunner.class)
-public class ServerOauth2ResourceApplicationTests {
-	@Autowired
-	private WebTestClient rest;
-
-	@Test
-	public void getWhenValidTokenThenIsOk() {
-		String token = "eyJhbGciOiJSUzI1NiJ9.eyJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6MzEwNjMyODEzMSwianRpIjoiOGY5ZjFiYzItOWVlMi00NTJkLThhMGEtODg3YmE4YmViYjYzIn0.CM_KulSsIrNXW1x6NFeN5VwKQiIW-LIAScJzakRFDox8Ql7o4WOb0ubY3CjWYnglwqYzBvH9McCFqVrUtzdfODY5tyEEJSxWndIGExOi2osrwRPsY3AGzNa23GMfC9I03BFP1IFCq4ZfL-L6yVcIjLke-rA40UG-r-oA7r-N_zsLc5poO7Azf29IQgQF0GSRp4AKQprYHF5Q-Nz9XkILMDz9CwPQ9cbdLCC9smvaGmEAjMUr-C1QgM-_ulb42gWtRDLorW_eArg8g-fmIP0_w82eNWCBjLTy-WaDMACnDVrrUVsUMCqx6jS6h8_uejKly2NFuhyueIHZTTySqCZoTA";
-		this.rest.get().uri("/")
-				.headers(headers -> headers.setBearerAuth(token))
-				.exchange()
-				.expectStatus().isOk()
-				.expectBody(String.class).isEqualTo("Hello, null!");
-	}
-
-	@Test
-	public void getWhenNoTokenThenIsUnauthorized() {
-		this.rest.get().uri("/")
-				.exchange()
-				.expectStatus().isUnauthorized()
-				.expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer");
-	}
-
-	@Test
-	public void getWhenNone() {
-		String token = "ew0KICAiYWxnIjogIm5vbmUiLA0KICAidHlwIjogIkpXVCINCn0.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJKb2huIERvZSIsDQogICJpYXQiOiAxNTE2MjM5MDIyDQp9.";
-		this.rest.get().uri("/")
-				.headers(headers -> headers.setBearerAuth(token))
-				.exchange()
-				.expectStatus().isUnauthorized()
-				.expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"Unsupported algorithm of none\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
-	}
-
-	@Test
-	public void getWhenInvalidToken() {
-		String token = "a";
-		this.rest.get().uri("/")
-				.headers(headers -> headers.setBearerAuth(token))
-				.exchange()
-				.expectStatus().isUnauthorized()
-				.expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"An error occurred while attempting to decode the Jwt: Invalid JWT serialization: Missing dot delimiter(s)\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
-	}
-}

+ 12 - 4
samples/boot/oauth2resourceserver/README.adoc

@@ -79,18 +79,26 @@ secret message
 
 
 _In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._
 _In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._
 
 
-_Additionally, remember that if your authorization server is running locally on port 8080, you'll need to change the sample's port in the `application.yml` by adding something like `server.port: 8082`._
-
 To change the sample to point at your Authorization Server, simply find this property in the `application.yml`:
 To change the sample to point at your Authorization Server, simply find this property in the `application.yml`:
 
 
 ```yaml
 ```yaml
-sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json
 ```
 ```
 
 
 And change the property to your Authorization Server's JWK set endpoint:
 And change the property to your Authorization Server's JWK set endpoint:
 
 
 ```yaml
 ```yaml
-sample.jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys
 ```
 ```
 
 
 And then you can run the app the same as before:
 And then you can run the app the same as before:

+ 0 - 1
samples/boot/oauth2resourceserver/src/integration-test/resources/application-test.yml

@@ -1 +0,0 @@
-sample.jwk-set-uri: mock://localhost:0/.well-known/jwks.json

+ 41 - 0
samples/boot/oauth2resourceserver/src/main/java/org/springframework/boot/env/MockWebServerEnvironmentPostProcessor.java

@@ -0,0 +1,41 @@
+/*
+ * 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 org.springframework.boot.env;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.boot.SpringApplication;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * @author Rob Winch
+ */
+public class MockWebServerEnvironmentPostProcessor
+		implements EnvironmentPostProcessor, DisposableBean {
+
+	private final MockWebServerPropertySource propertySource = new MockWebServerPropertySource();
+
+	@Override
+	public void postProcessEnvironment(ConfigurableEnvironment environment,
+			SpringApplication application) {
+		environment.getPropertySources().addFirst(this.propertySource);
+	}
+
+	@Override
+	public void destroy() throws Exception {
+		this.propertySource.destroy();
+	}
+}

+ 121 - 0
samples/boot/oauth2resourceserver/src/main/java/org/springframework/boot/env/MockWebServerPropertySource.java

@@ -0,0 +1,121 @@
+/*
+ * 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 org.springframework.boot.env;
+
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.core.env.PropertySource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+import java.io.IOException;
+
+/**
+ * @author Rob Winch
+ */
+public class MockWebServerPropertySource extends PropertySource<MockWebServer> implements
+		DisposableBean {
+
+	private static final MockResponse JWKS_RESPONSE = response(
+			"{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMwDBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK48M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6oXZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQPDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkFk5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrnuYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHsV9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC58CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eqD9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}",
+			200
+	);
+
+	private static final MockResponse NOT_FOUND_RESPONSE = response(
+			"{ \"message\" : \"This mock authorization server responds to just one request: GET /.well-known/jwks.json.\" }",
+			404
+	);
+
+	/**
+	 * Name of the random {@link PropertySource}.
+	 */
+	public static final String MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME = "mockwebserver";
+
+	private static final String NAME = "mockwebserver.url";
+
+	private static final Log logger = LogFactory.getLog(MockWebServerPropertySource.class);
+
+	private boolean started;
+
+	public MockWebServerPropertySource() {
+		super(MOCK_WEB_SERVER_PROPERTY_SOURCE_NAME, new MockWebServer());
+	}
+
+	@Override
+	public Object getProperty(String name) {
+		if (!name.equals(NAME)) {
+			return null;
+		}
+		if (logger.isTraceEnabled()) {
+			logger.trace("Looking up the url for '" + name + "'");
+		}
+		String url = getUrl();
+		return url;
+	}
+
+	@Override
+	public void destroy() throws Exception {
+		getSource().shutdown();
+	}
+
+	/**
+	 * Get's the URL (i.e. "http://localhost:123456")
+	 * @return
+	 */
+	private String getUrl() {
+		MockWebServer mockWebServer = getSource();
+		if (!this.started) {
+			intializeMockWebServer(mockWebServer);
+		}
+		String url = mockWebServer.url("").url().toExternalForm();
+		return url.substring(0, url.length() - 1);
+	}
+
+	private void intializeMockWebServer(MockWebServer mockWebServer) {
+		Dispatcher dispatcher = new Dispatcher() {
+			@Override
+			public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+				if ("/.well-known/jwks.json".equals(request.getPath())) {
+					return JWKS_RESPONSE;
+				}
+
+				return NOT_FOUND_RESPONSE;
+			}
+		};
+
+		mockWebServer.setDispatcher(dispatcher);
+		try {
+			mockWebServer.start();
+			this.started = true;
+		} catch (IOException e) {
+			throw new RuntimeException("Could not start " + mockWebServer, e);
+		}
+	}
+
+	private static MockResponse response(String body, int status) {
+		return new MockResponse()
+				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+				.setResponseCode(status)
+				.setBody(body);
+	}
+
+}

+ 22 - 0
samples/boot/oauth2resourceserver/src/main/java/org/springframework/boot/env/package-info.java

@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * This provides integration of a {@link okhttp3.mockwebserver.MockWebServer} and the
+ * {@link org.springframework.core.env.Environment}
+ * @author Rob Winch
+ */
+package org.springframework.boot.env;

+ 2 - 6
samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java

@@ -15,7 +15,6 @@
  */
  */
 package sample;
 package sample;
 
 
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 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.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@@ -25,20 +24,17 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
  */
  */
 @EnableWebSecurity
 @EnableWebSecurity
 public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
 public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
-	@Value("${sample.jwk-set-uri}")
-	String jwkSetUri;
 
 
 	@Override
 	@Override
 	protected void configure(HttpSecurity http) throws Exception {
 	protected void configure(HttpSecurity http) throws Exception {
 		// @formatter:off
 		// @formatter:off
 		http
 		http
 			.authorizeRequests()
 			.authorizeRequests()
-				.antMatchers("/message/**").access("hasAuthority('SCOPE_message:read')")
+				.antMatchers("/message/**").hasAuthority("SCOPE_message:read")
 				.anyRequest().authenticated()
 				.anyRequest().authenticated()
 				.and()
 				.and()
 			.oauth2ResourceServer()
 			.oauth2ResourceServer()
-				.jwt()
-					.jwkSetUri(this.jwkSetUri);
+				.jwt();
 		// @formatter:on
 		// @formatter:on
 	}
 	}
 }
 }

+ 1 - 1
samples/boot/oauth2resourceserver/src/main/resources/META-INF/spring.factories

@@ -1 +1 @@
-org.springframework.boot.env.EnvironmentPostProcessor=sample.provider.MockProvider
+org.springframework.boot.env.EnvironmentPostProcessor=org.springframework.boot.env.MockWebServerEnvironmentPostProcessor

+ 6 - 1
samples/boot/oauth2resourceserver/src/main/resources/application.yml

@@ -1 +1,6 @@
-sample.jwk-set-uri: mock://localhost:8081/.well-known/jwks.json
+spring:
+  security:
+    oauth2:
+      resourceserver:
+        jwt:
+          jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json