mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-30 22:28:46 +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