Polish resourceserver samples

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

Fixes: gh-5844
This commit is contained in:
Rob Winch 2018-09-13 14:31:23 -05:00
parent f68141d42a
commit c21b2f31c6
19 changed files with 473 additions and 298 deletions

View File

@ -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 default, the tests are pointing at a mock Authorization Server instance.
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 The tests are configured with a set of hard-coded tokens originally obtained from the mock Authorization Server,
authorizes the request, returning the phrase 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
```
And then make this request:
```bash
curl -H "Authorization: Bearer $TOKEN" localhost:8080
``` ```
Which will respond with the phrase: Which will respond with 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.
=== How do I obtain a valid JWT token? Or this:
Getting a valid JWT token from an Authorization Server will vary, depending on your setup. However, it will typically
look something like this:
```bash ```bash
curl --user {client id}:{client password} -d "grant_type=client_credentials" {auth server endpoint}/token export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A
curl -H "Authorization: Bearer $TOKEN" localhost:8080/message
``` ```
which will respond with a JSON payload containing the `access_token` among other things: Will respond with:
```bash ```bash
{ "access_token" : "{the access token}", "token_type" : "Bearer", "expires_in" : "{an expiry}", "scope" : "{a list of scopes}" } secret message
``` ```
For example, the following can be used to hit the sample Okta endpoint for a valid JWT token: == 2. Testing against other Authorization Servers
```bash _In order to use this sample, your Authorization Server must support JWTs that either use the "scope" or "scp" attribute._
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): To change the sample to point at your Authorization Server, simply find this property in the `application.yml`:
```json
{
"access_token": "eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "ok"
}
```
Then, using that access token:
```bash
curl -H "Authorization: Bearer eyJraWQiOiJFRjBFWDFFWHZGc1hGaDhuYkRGazNJN0hMUDBsZnJnc0JKMVdBWmkwRmI0IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmtQSUdfMEVMQmM3NVFMN3c4ZHBMVFRtNXZFVFd3d1R2dzJ3aXNISGRMbjgiLCJpc3MiOiJodHRwczovL2Rldi04MDUyNjIub2t0YXByZXZpZXcuY29tL29hdXRoMi9kZWZhdWx0IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwiaWF0IjoxNTI4ODYwMTkxLCJleHAiOjE1Mjg4NjM3OTEsImNpZCI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3Iiwic2NwIjpbIm9rIl0sInN1YiI6IjBvYWY1dTVnNG02Q1c0eDZ6MGg3In0.G_F9MQ3pqCy-YwfcNhryoPG5E1q4tQ7gV8OIDizR3QouUgrqT7MQsLQCTtGGLF2Fi0qq0Pr-V-wWa2MkyvcboEAhnfYi4rd3UmMrRTrNana6pVZjVWB_uj88-mZ57lFRnoYMCFbepmCxmY6D6p354H964xXWdtY7d6fw7F88DRDWMGQE0iQjMuUDg4izptVcK9db7uMonYTT1PFvOBQfwcn1zCeDVQgZFe7gjQA71CV9M6CIAXYDrpzp_hs95xco7Q3ncN3J7ZkCebLcUL6MdJS2nVuX6D6eC9PrtmCj06mb0-ydlzBSIUCPMaMQk9EhlEM_qK3d1iimCQnwo6KsIQ" \
localhost:8081
```
I get:
```bash
Hello, 0oaf5u5g4m6CW4x6z0h7!
```
== 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:
```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 And change the property to your Authorization Server's JWK set endpoint:
already prepared with the one for Okta commented out:
```yaml ```yaml
# master: #keycloak spring:
# issuer: http://localhost:8080/auth/realms/master security:
# jwk-set-uri: http://localhost:8080/auth/realms/master/protocol/openid-connect/certs oauth2:
okta: resourceserver:
issuer: https://dev-805262.oktapreview.com/oauth2/default jwt:
jwk-set-uri: https://dev-805262.oktapreview.com/oauth2/default/v1/keys jwk-set-uri: https://dev-123456.oktapreview.com/oauth2/default/v1/keys
``` ```
Comment out the `okta` section and uncomment the desired section. And then you can run the app the same as before:
Second, find the following section, which the sample needs in order to retreive a valid token from the Authorization ```bash
Server: ./gradlew bootRun
```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. 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.
=== How can I test with my own Authorization Server instance? To use the `/message` endpoint, the token should have the `message:read` scope.
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
```
And indicate, using the sample.provider properties, how the sample should generate a valid JWT token:
```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
```
You can provide values for any OAuth 2.0-compliant Authorization Server.

View File

@ -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'

View File

@ -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\""));
}
}

View File

@ -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();
}
}

View File

@ -14,47 +14,26 @@
* limitations under the License. * limitations under the License.
*/ */
package sample.provider; package org.springframework.boot.env;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PreDestroy;
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.apache.commons.logging.Log;
import org.springframework.boot.SpringApplication; import org.apache.commons.logging.LogFactory;
import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource;
import org.springframework.core.env.MapPropertySource;
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). * @author Rob Winch
*
* 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
*/ */
public class MockProvider implements EnvironmentPostProcessor { public class MockWebServerPropertySource extends PropertySource<MockWebServer> implements
private MockWebServer server = new MockWebServer(); 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); mockWebServer.setDispatcher(dispatcher);
} try {
mockWebServer.start();
@Override this.started = true;
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { } catch (IOException e) {
String uri = environment.getProperty("sample.jwk-set-uri", "mock://localhost:0"); throw new RuntimeException("Could not start " + mockWebServer, e);
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);
} }
} }
@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);
} }
} }

View File

@ -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;

View File

@ -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";
}
} }

View File

@ -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) throws Exception {
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, @Value("${sample.jwk-set-uri}") String jwkSetUri) throws Exception {
http http
.authorizeExchange() .authorizeExchange()
.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
.anyExchange().authenticated() .anyExchange().authenticated()
.and() .and()
.oauth2ResourceServer() .oauth2ResourceServer()
.jwt() .jwt();
.jwkSetUri(jwkSetUri);
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);
}
} }

View File

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

View File

@ -1,11 +1,6 @@
logging: spring:
level: security:
root: INFO oauth2:
org.springframework.web: INFO resourceserver:
org.springframework.security: INFO jwt:
# org.springframework.boot.autoconfigure: DEBUG jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json
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

View File

@ -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\"");
}
}

View File

@ -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:

View File

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

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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() .jwt();
.jwkSetUri(this.jwkSetUri);
// @formatter:on // @formatter:on
} }
} }

View File

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

View File

@ -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