浏览代码

Polish resourceserver samples

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

Fixes: gh-5844
Rob Winch 7 年之前
父节点
当前提交
c21b2f31c6
共有 19 个文件被更改,包括 470 次插入295 次删除
  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
 
-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
 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:
 
 ```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?
 
-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
-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
 
 To run as a stand-alone application, do:
 
 ```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
-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
-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
-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
-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
-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
 spring:
   security:
     oauth2:
       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
-#          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 'org.springframework.boot:spring-boot-starter-webflux'
+	compile 'com.squareup.okhttp3:mockwebserver'
 
 	testCompile project(':spring-security-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.
  */
 
-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.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 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.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(
 			"{\"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
 	);
 
-	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() {
 			@Override
 			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) {
 		return new MockResponse()
 				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
 				.setResponseCode(status)
 				.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) {
 		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;
 
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 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
  * @since 5.1
  */
 @EnableWebFluxSecurity
 public class SecurityConfig {
-	private static final String JWK_SET_URI_PROP = "sample.jwk-set-uri";
 
 	@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
 			.authorizeExchange()
+				.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
 				.anyExchange().authenticated()
 				.and()
 			.oauth2ResourceServer()
-				.jwt()
-					.jwkSetUri(jwkSetUri);
+				.jwt();
 		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._
 
-_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`:
 
 ```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:
 
 ```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:

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