mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-31 06:38:42 +00:00 
			
		
		
		
	Resource Server JWE Sample
Issue: gh-4435
This commit is contained in:
		
							parent
							
								
									37d108ccc2
								
							
						
					
					
						commit
						ecb13aa8cc
					
				
							
								
								
									
										119
									
								
								samples/boot/oauth2resourceserver-jwe/README.adoc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								samples/boot/oauth2resourceserver-jwe/README.adoc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | ||||
| = OAuth 2.0 Resource Server Sample | ||||
| 
 | ||||
| This sample demonstrates integrating Resource Server with a mock Authorization Server, though it can be modified to integrate | ||||
| with your favorite Authorization Server. This resource server is configured to accept JWE-encrypted tokens. | ||||
| 
 | ||||
| With it, you can run the integration tests or run the application as a stand-alone service to explore how you can | ||||
| secure your own service with OAuth 2.0 Bearer Tokens using Spring Security. | ||||
| 
 | ||||
| == 1. Running the tests | ||||
| 
 | ||||
| To run the tests, do: | ||||
| 
 | ||||
| ```bash | ||||
| ./gradlew integrationTest | ||||
| ``` | ||||
| 
 | ||||
| Or import the project into your IDE and run `OAuth2ResourceServerApplicationTests` from there. | ||||
| 
 | ||||
| === What is it doing? | ||||
| 
 | ||||
| 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 decrypts the token and subsquently verifies it with the Authorization Server and authorizes the request, returning the phrase | ||||
| 
 | ||||
| ```bash | ||||
| Hello, subject! | ||||
| ``` | ||||
| 
 | ||||
| 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 | ||||
| ``` | ||||
| 
 | ||||
| Or import the project into your IDE and run `OAuth2ResourceServerApplication` from there. | ||||
| 
 | ||||
| Once it is up, you can use the following token: | ||||
| 
 | ||||
| ```bash | ||||
| export TOKEN=eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.IyeWtsTonaiWJdoT13B0M7gpqVxAirVGlfqFI4TOmTRcVHICs_ESezS7fa0ODS9XYdwklTtG7hH39yeeMzr2Zo1Ghh-m36fdoqQrV1Do04rUvuTjqbgyNffeZEGB6rquJ-cyAVjp_Oljy10-Bbnw7CeVGwNBSVo9UCB5j49OlNWhLxFpYARFmOlYpXj-s4Q4JiqV6EvjDAYeohAR4QQmND3qoxR-s2I6SLcIho0sSSpUlhrRiqu2uvWefHDcZJdW2WYWnxLHxhzNu3CfnLiqhhaA_YA_iWXR9FYnPDCf_4q3FgSXcgttXzomFKAx5DwnE_dXvsCvpWxslZMU1UIiLA.MHOrrza2GQ9_5PIv.zU4tfhxT6apWBC3stBwQmGlCQBltWVQe4dFIykybWWBFqxo1bf2BZ37twzoEIFXG9jSYEMH4mvBXPWSvn66t-_jnqLnKTJst2plBjhagGCAoLNWXVKeYNp67o-lKOD_JJQFqsRw4oE05VSgRr14MZeaUBFcU3A_kKxMXOu899DKfXBGJvj75H7lDyd8RUXTb-OSWWfUiJc6Y5AUy1zCZCN9yfDsCXt9heTsZANy92Oou9sMFaXkYzyums5OtkBtLFzyuNMEoNioRehTV-FTuL8tDRB1mNhHObwsBfFbR6M1jJK37pHUXGtko-yZ6NGwxyLtwGh4uU2jzE614rQzuzR8aHaHxOkUs1pBTZ8AcRt41snByOe-KU0adthHxedobFiQQBoQ05DgSU7DO6hsK0uVBDF3eG2KjH4L2lZy-WugloLHhdguUoO9F0zUx15-XZO4EVzmhy8xfH2tSXz98eKzz9Dv0DdGnrBL9cK2MM88N1zoq5u4NdlnE12HvuesB7GKdMwZx1-gTw_pzP81TzcctJWl6ETK09Uc.jk0O8qc4Fvip856stDz05Q | ||||
| ``` | ||||
| 
 | ||||
| And then make this request: | ||||
| 
 | ||||
| ```bash | ||||
| curl -H "Authorization: Bearer $TOKEN" localhost:8080 | ||||
| ``` | ||||
| 
 | ||||
| Which will respond with the phrase: | ||||
| 
 | ||||
| ```bash | ||||
| Hello, subject! | ||||
| ``` | ||||
| 
 | ||||
| where `subject` is the value of the `sub` field in the JWT returned by the Authorization Server. | ||||
| 
 | ||||
| Or this: | ||||
| 
 | ||||
| ```bash | ||||
| export TOKEN=eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.CRBAEgvQhpB6pPQhpkTAKpsDai1FDcvkDSRig1R3OI-g18JdTe-qDhzWwP-hV3aCwFwHxQ_g8Z8OIZvhyKpQaPwBb72UeLqqhzSIkm0gEsmmjYg1vEGOrDH5_Fqlm0LnAnXTmsbOIWYIj11ZuenI2lEmMCkVwqth0RlzakdcHRXiuDTEln_trhZpE2j80X-9rS2gZy9Raa9VLir3-F3wC0GKPEL6e3x1OygC03ix9uyXS3vpTsU9zlgoYADZyaLeOF1mCG4mQhvXs7IPmPbwNsElJwKh0xSQCHvNOQShprlvd3cHiUFKYw9fXphY1O-AUYcRzHk4DjoBdkGNQMy_Kw.KtC_z674rYBtDgkN.e8QU50Iq1JHkn-1USSxpjEkbrukb4cobvlQRK40iXGAKVIuOod4bSq5fDpIAPHugqIf-_zGsvr-2OCOdzhtBikL46wU7UdZppxPWtk-X6kl33zH_XObRMaGfe-hLxt3RPxRVn7I1Hp6tGW1Rkxyf_ESq4XlcbbrkhDoIz_G_LKXJhvQ-xahW2e0AUc7RZSucns4XUeq9xX_dd7Ht-o1TmQI9WFoFc1l7oh9GtQ6GZMsghnZ1VrbIS2L7jSYiSsD2JqSv1LLtOGj_FBA0ufhqM3LloGiwflEwAryMD10oNb73WonKEycEj1rBsTIKW7SHkI-VkrQA4-8N-aLWgHwDnzyPZmyNyKpqUMvhjIE_0w6oqU4HpN7J5nfBEIAtpPZ_pDkwAdxCQ7JV3zfiUnF7ZQ3q1PnSId315si02ZN9-wRSrMHcHnboQN1Hs4xCAfGyClVyLpCzfa_fAehjt6v1DjgjbzwSjr_LdNmWTvXYBhNO8Jq9Vb7axksrdwksD3pYNMY8cRZxP-LO0V5Sv1_kT_Hf2yLo2iTwB8n8szzGrJ4QQLb5Znu7Sv-M2x52cnIDMiorP3LNpFk.G85FuMSm-8bGumFAStiFQA | ||||
| 
 | ||||
| curl -H "Authorization: Bearer $TOKEN" localhost:8080/message | ||||
| ``` | ||||
| 
 | ||||
| Will respond with: | ||||
| 
 | ||||
| ```bash | ||||
| secret message | ||||
| ``` | ||||
| 
 | ||||
| == 2. Testing against other Authorization Servers | ||||
| 
 | ||||
| _In order to use this sample, your Authorization Server must encrypt using the public key available in the sample. | ||||
| Also it must support JWTs that either use the "scope" or "scp" attribute._ | ||||
| 
 | ||||
| To change the sample to point at your Authorization Server, simply find this property in the `application.yml`: | ||||
| 
 | ||||
| ```yaml | ||||
| 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 | ||||
| spring: | ||||
|   security: | ||||
|     oauth2: | ||||
|       resourceserver: | ||||
|         jwt: | ||||
|           jwk-set-uri: https://localhost:9031/pf/JWKS | ||||
| ``` | ||||
| 
 | ||||
| If your Authorization Server does not support RSA_OAEP_256 or AESGCM, then you can change these values in `OAuth2ResourceServerSecurityConfiguration`: | ||||
| 
 | ||||
| ```java | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| And then you can run the app the same as before: | ||||
| 
 | ||||
| ```bash | ||||
| ./gradlew bootRun | ||||
| ``` | ||||
| 
 | ||||
| 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. | ||||
| @ -0,0 +1,13 @@ | ||||
| apply plugin: 'io.spring.convention.spring-sample-boot' | ||||
| 
 | ||||
| dependencies { | ||||
| 	compile project(':spring-security-config') | ||||
| 	compile project(':spring-security-oauth2-jose') | ||||
| 	compile project(':spring-security-oauth2-resource-server') | ||||
| 
 | ||||
| 	compile 'org.springframework.boot:spring-boot-starter-web' | ||||
| 	compile 'com.squareup.okhttp3:mockwebserver' | ||||
| 
 | ||||
| 	testCompile project(':spring-security-test') | ||||
| 	testCompile 'org.springframework.boot:spring-boot-starter-test' | ||||
| } | ||||
| @ -0,0 +1,101 @@ | ||||
| /* | ||||
|  * Copyright 2002-2017 the original author or authors. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package sample; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| 
 | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | ||||
| import org.springframework.boot.test.context.SpringBootTest; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| import org.springframework.mock.web.MockHttpServletRequest; | ||||
| import org.springframework.test.context.ActiveProfiles; | ||||
| import org.springframework.test.context.junit4.SpringRunner; | ||||
| import org.springframework.test.web.servlet.MockMvc; | ||||
| import org.springframework.test.web.servlet.request.RequestPostProcessor; | ||||
| 
 | ||||
| import static org.hamcrest.Matchers.containsString; | ||||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; | ||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; | ||||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||||
| 
 | ||||
| /** | ||||
|  * Integration tests for {@link OAuth2ResourceServerApplication} | ||||
|  * | ||||
|  * @author Josh Cummings | ||||
|  */ | ||||
| @RunWith(SpringRunner.class) | ||||
| @SpringBootTest | ||||
| @AutoConfigureMockMvc | ||||
| @ActiveProfiles("test") | ||||
| public class OAuth2ResourceServerApplicationITests { | ||||
| 
 | ||||
| 	String noScopesToken = "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.IyeWtsTonaiWJdoT13B0M7gpqVxAirVGlfqFI4TOmTRcVHICs_ESezS7fa0ODS9XYdwklTtG7hH39yeeMzr2Zo1Ghh-m36fdoqQrV1Do04rUvuTjqbgyNffeZEGB6rquJ-cyAVjp_Oljy10-Bbnw7CeVGwNBSVo9UCB5j49OlNWhLxFpYARFmOlYpXj-s4Q4JiqV6EvjDAYeohAR4QQmND3qoxR-s2I6SLcIho0sSSpUlhrRiqu2uvWefHDcZJdW2WYWnxLHxhzNu3CfnLiqhhaA_YA_iWXR9FYnPDCf_4q3FgSXcgttXzomFKAx5DwnE_dXvsCvpWxslZMU1UIiLA.MHOrrza2GQ9_5PIv.zU4tfhxT6apWBC3stBwQmGlCQBltWVQe4dFIykybWWBFqxo1bf2BZ37twzoEIFXG9jSYEMH4mvBXPWSvn66t-_jnqLnKTJst2plBjhagGCAoLNWXVKeYNp67o-lKOD_JJQFqsRw4oE05VSgRr14MZeaUBFcU3A_kKxMXOu899DKfXBGJvj75H7lDyd8RUXTb-OSWWfUiJc6Y5AUy1zCZCN9yfDsCXt9heTsZANy92Oou9sMFaXkYzyums5OtkBtLFzyuNMEoNioRehTV-FTuL8tDRB1mNhHObwsBfFbR6M1jJK37pHUXGtko-yZ6NGwxyLtwGh4uU2jzE614rQzuzR8aHaHxOkUs1pBTZ8AcRt41snByOe-KU0adthHxedobFiQQBoQ05DgSU7DO6hsK0uVBDF3eG2KjH4L2lZy-WugloLHhdguUoO9F0zUx15-XZO4EVzmhy8xfH2tSXz98eKzz9Dv0DdGnrBL9cK2MM88N1zoq5u4NdlnE12HvuesB7GKdMwZx1-gTw_pzP81TzcctJWl6ETK09Uc.jk0O8qc4Fvip856stDz05Q"; | ||||
| 	String messageReadToken = "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.CRBAEgvQhpB6pPQhpkTAKpsDai1FDcvkDSRig1R3OI-g18JdTe-qDhzWwP-hV3aCwFwHxQ_g8Z8OIZvhyKpQaPwBb72UeLqqhzSIkm0gEsmmjYg1vEGOrDH5_Fqlm0LnAnXTmsbOIWYIj11ZuenI2lEmMCkVwqth0RlzakdcHRXiuDTEln_trhZpE2j80X-9rS2gZy9Raa9VLir3-F3wC0GKPEL6e3x1OygC03ix9uyXS3vpTsU9zlgoYADZyaLeOF1mCG4mQhvXs7IPmPbwNsElJwKh0xSQCHvNOQShprlvd3cHiUFKYw9fXphY1O-AUYcRzHk4DjoBdkGNQMy_Kw.KtC_z674rYBtDgkN.e8QU50Iq1JHkn-1USSxpjEkbrukb4cobvlQRK40iXGAKVIuOod4bSq5fDpIAPHugqIf-_zGsvr-2OCOdzhtBikL46wU7UdZppxPWtk-X6kl33zH_XObRMaGfe-hLxt3RPxRVn7I1Hp6tGW1Rkxyf_ESq4XlcbbrkhDoIz_G_LKXJhvQ-xahW2e0AUc7RZSucns4XUeq9xX_dd7Ht-o1TmQI9WFoFc1l7oh9GtQ6GZMsghnZ1VrbIS2L7jSYiSsD2JqSv1LLtOGj_FBA0ufhqM3LloGiwflEwAryMD10oNb73WonKEycEj1rBsTIKW7SHkI-VkrQA4-8N-aLWgHwDnzyPZmyNyKpqUMvhjIE_0w6oqU4HpN7J5nfBEIAtpPZ_pDkwAdxCQ7JV3zfiUnF7ZQ3q1PnSId315si02ZN9-wRSrMHcHnboQN1Hs4xCAfGyClVyLpCzfa_fAehjt6v1DjgjbzwSjr_LdNmWTvXYBhNO8Jq9Vb7axksrdwksD3pYNMY8cRZxP-LO0V5Sv1_kT_Hf2yLo2iTwB8n8szzGrJ4QQLb5Znu7Sv-M2x52cnIDMiorP3LNpFk.G85FuMSm-8bGumFAStiFQA"; | ||||
| 
 | ||||
| 	@Autowired | ||||
| 	MockMvc mvc; | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void performWhenValidBearerTokenThenAllows() | ||||
| 		throws Exception { | ||||
| 
 | ||||
| 		this.mvc.perform(get("/").with(bearerToken(this.noScopesToken))) | ||||
| 				.andExpect(status().isOk()) | ||||
| 				.andExpect(content().string(containsString("Hello, subject!"))); | ||||
| 	} | ||||
| 
 | ||||
| 	// -- tests with scopes | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void performWhenValidBearerTokenThenScopedRequestsAlsoWork() | ||||
| 			throws Exception { | ||||
| 
 | ||||
| 		this.mvc.perform(get("/message").with(bearerToken(this.messageReadToken))) | ||||
| 				.andExpect(status().isOk()) | ||||
| 				.andExpect(content().string(containsString("secret message"))); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	public void performWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() | ||||
| 			throws Exception { | ||||
| 
 | ||||
| 		this.mvc.perform(get("/message").with(bearerToken(this.noScopesToken))) | ||||
| 				.andExpect(status().isForbidden()) | ||||
| 				.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, | ||||
| 						containsString("Bearer error=\"insufficient_scope\""))); | ||||
| 	} | ||||
| 
 | ||||
| 	private static class BearerTokenRequestPostProcessor implements RequestPostProcessor { | ||||
| 		private String token; | ||||
| 
 | ||||
| 		public BearerTokenRequestPostProcessor(String token) { | ||||
| 			this.token = token; | ||||
| 		} | ||||
| 
 | ||||
| 		@Override | ||||
| 		public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { | ||||
| 			request.addHeader("Authorization", "Bearer " + this.token); | ||||
| 			return request; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private static BearerTokenRequestPostProcessor bearerToken(String token) { | ||||
| 		return new BearerTokenRequestPostProcessor(token); | ||||
| 	} | ||||
| } | ||||
| @ -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 | ||||
|  * | ||||
|  *      https://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(); | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,122 @@ | ||||
| /* | ||||
|  * 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 | ||||
|  * | ||||
|  *      https://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 java.io.IOException; | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
| /** | ||||
|  * @author Rob Winch | ||||
|  */ | ||||
| public class MockWebServerPropertySource extends PropertySource<MockWebServer> implements | ||||
| 		DisposableBean { | ||||
| 
 | ||||
| 	private static final MockResponse JWKS_RESPONSE = response( | ||||
| 			"{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"n\":\"i7H90yfquGVhXxekdzXkMaxlIg67Q_ofd7iuFHtgeUx-Iye2QjukuULhl774oITYnZIZsh2UHxRYG8nFypcYZfHJMQes_OYFTkTvRroKll5p3wxSkhpARbkEPFMyMJ5WIm3MeNO2ermMhDWVVeI2xQH-tW6w-C6b5d_F6lrIwCnpZwSv6PQ3kef-rcObp_PZANIo232bvpwyC6uW1W2kpjAvYJhQ8NrkG2oO0ynqEJW2UyoCWRdsT2BLZcFMAcxG3Iw9b9__IbvNoUBwr596JYfzrX0atiKyk4Yg8dJ1wBjHFN2fkHTlzn6HDwTJkj4VNDQvKu4P2zhKn1gmWuxhuQ\"}]}", | ||||
| 			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); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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 | ||||
|  * | ||||
|  *      https://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; | ||||
| @ -0,0 +1,30 @@ | ||||
| /* | ||||
|  * Copyright 2002-2018 the original author or authors. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package sample; | ||||
| 
 | ||||
| import org.springframework.boot.SpringApplication; | ||||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||||
| 
 | ||||
| /** | ||||
|  * @author Josh Cummings | ||||
|  */ | ||||
| @SpringBootApplication | ||||
| public class OAuth2ResourceServerApplication { | ||||
| 
 | ||||
| 	public static void main(String[] args) { | ||||
| 		SpringApplication.run(OAuth2ResourceServerApplication.class, args); | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| /* | ||||
|  * Copyright 2002-2018 the original author or authors. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package sample; | ||||
| 
 | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||||
| import org.springframework.security.oauth2.jwt.Jwt; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| 
 | ||||
| /** | ||||
|  * @author Josh Cummings | ||||
|  */ | ||||
| @RestController | ||||
| public class OAuth2ResourceServerController { | ||||
| 
 | ||||
| 	@GetMapping("/") | ||||
| 	public String index(@AuthenticationPrincipal Jwt jwt) { | ||||
| 		return String.format("Hello, %s!", jwt.getSubject()); | ||||
| 	} | ||||
| 
 | ||||
| 	@GetMapping("/message") | ||||
| 	public String message() { | ||||
| 		return "secret message"; | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,108 @@ | ||||
| /* | ||||
|  * 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 | ||||
|  * | ||||
|  *      https://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| package sample; | ||||
| 
 | ||||
| import java.net.URL; | ||||
| import java.security.interfaces.RSAPrivateCrtKey; | ||||
| import java.security.interfaces.RSAPrivateKey; | ||||
| 
 | ||||
| import com.nimbusds.jose.EncryptionMethod; | ||||
| import com.nimbusds.jose.JWEAlgorithm; | ||||
| import com.nimbusds.jose.JWSAlgorithm; | ||||
| import com.nimbusds.jose.jwk.JWKSet; | ||||
| import com.nimbusds.jose.jwk.KeyUse; | ||||
| import com.nimbusds.jose.jwk.RSAKey; | ||||
| import com.nimbusds.jose.jwk.source.ImmutableJWKSet; | ||||
| import com.nimbusds.jose.jwk.source.JWKSource; | ||||
| import com.nimbusds.jose.jwk.source.RemoteJWKSet; | ||||
| import com.nimbusds.jose.proc.JWEDecryptionKeySelector; | ||||
| import com.nimbusds.jose.proc.JWEKeySelector; | ||||
| import com.nimbusds.jose.proc.JWSKeySelector; | ||||
| import com.nimbusds.jose.proc.JWSVerificationKeySelector; | ||||
| import com.nimbusds.jose.proc.SecurityContext; | ||||
| import com.nimbusds.jose.util.Base64URL; | ||||
| import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; | ||||
| import com.nimbusds.jwt.proc.DefaultJWTProcessor; | ||||
| import com.nimbusds.jwt.proc.JWTProcessor; | ||||
| 
 | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||||
| import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | ||||
| import org.springframework.security.oauth2.jwt.JwtDecoder; | ||||
| import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; | ||||
| 
 | ||||
| /** | ||||
|  * @author Josh Cummings | ||||
|  */ | ||||
| @EnableWebSecurity | ||||
| public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter { | ||||
| 
 | ||||
| 	private final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256; | ||||
| 	private final JWEAlgorithm jweAlgorithm = JWEAlgorithm.RSA_OAEP_256; | ||||
| 	private final EncryptionMethod encryptionMethod = EncryptionMethod.A256GCM; | ||||
| 
 | ||||
| 	@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") | ||||
| 	URL jwkSetUri; | ||||
| 
 | ||||
| 	@Value("${sample.jwe-key-value}") | ||||
| 	RSAPrivateKey key; | ||||
| 
 | ||||
| 	@Override | ||||
| 	protected void configure(HttpSecurity http) throws Exception { | ||||
| 		// @formatter:off | ||||
| 		http | ||||
| 			.authorizeRequests() | ||||
| 				.antMatchers("/message/**").hasAuthority("SCOPE_message:read") | ||||
| 				.anyRequest().authenticated() | ||||
| 				.and() | ||||
| 			.oauth2ResourceServer() | ||||
| 				.jwt(); | ||||
| 		// @formatter:on | ||||
| 	} | ||||
| 
 | ||||
| 	@Bean | ||||
| 	JwtDecoder jwtDecoder() { | ||||
| 		return new NimbusJwtDecoder(jwtProcessor()); | ||||
| 	} | ||||
| 
 | ||||
| 	private JWTProcessor<SecurityContext> jwtProcessor() { | ||||
| 		JWKSource<SecurityContext> jwsJwkSource = new RemoteJWKSet<>(this.jwkSetUri); | ||||
| 		JWSKeySelector<SecurityContext> jwsKeySelector = | ||||
| 				new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwsJwkSource); | ||||
| 
 | ||||
| 		JWKSource<SecurityContext> jweJwkSource = new ImmutableJWKSet<>(new JWKSet(rsaKey())); | ||||
| 		JWEKeySelector<SecurityContext> jweKeySelector = | ||||
| 				new JWEDecryptionKeySelector<>(this.jweAlgorithm, this.encryptionMethod, jweJwkSource); | ||||
| 
 | ||||
| 		ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>(); | ||||
| 		jwtProcessor.setJWSKeySelector(jwsKeySelector); | ||||
| 		jwtProcessor.setJWEKeySelector(jweKeySelector); | ||||
| 
 | ||||
| 		return jwtProcessor; | ||||
| 	} | ||||
| 
 | ||||
| 	private RSAKey rsaKey() { | ||||
| 		RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key; | ||||
| 		Base64URL n = Base64URL.encode(crtKey.getModulus()); | ||||
| 		Base64URL e = Base64URL.encode(crtKey.getPublicExponent()); | ||||
| 		return new RSAKey.Builder(n, e) | ||||
| 				.privateKey(this.key) | ||||
| 				.keyUse(KeyUse.ENCRYPTION) | ||||
| 				.build(); | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| org.springframework.boot.env.EnvironmentPostProcessor=org.springframework.boot.env.MockWebServerEnvironmentPostProcessor | ||||
| @ -0,0 +1,9 @@ | ||||
| spring: | ||||
|   security: | ||||
|     oauth2: | ||||
|       resourceserver: | ||||
|         jwt: | ||||
|           jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json | ||||
| 
 | ||||
| sample: | ||||
|   jwe-key-value: classpath:simple.priv | ||||
| @ -0,0 +1,28 @@ | ||||
| -----BEGIN PRIVATE KEY----- | ||||
| MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA | ||||
| iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM | ||||
| g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK | ||||
| LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF | ||||
| oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc | ||||
| 3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn | ||||
| +jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE | ||||
| E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek | ||||
| lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG | ||||
| mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7 | ||||
| 62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0 | ||||
| bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA | ||||
| +Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH | ||||
| Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA | ||||
| 8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd | ||||
| I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY | ||||
| QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d | ||||
| rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk | ||||
| HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA | ||||
| Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN | ||||
| HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a | ||||
| FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF | ||||
| snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H | ||||
| c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM | ||||
| TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR | ||||
| 47jndeyIaMTNETEmOnms+as17g== | ||||
| -----END PRIVATE KEY----- | ||||
| @ -0,0 +1,9 @@ | ||||
| -----BEGIN PUBLIC KEY----- | ||||
| MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd | ||||
| 7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv | ||||
| c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6 | ||||
| iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2 | ||||
| kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o | ||||
| RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj | ||||
| KwIDAQAB | ||||
| -----END PUBLIC KEY----- | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user