diff --git a/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerProperties.java b/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerProperties.java index 97fc9d41..77743614 100644 --- a/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerProperties.java +++ b/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -115,7 +115,7 @@ public class Jwt { private String keyValue; /** - * The URI of the JWT token. Can be set if the value is not available and the key is public. + * The URI of the JWT token. Can be set if the value is not available. */ private String keyUri; diff --git a/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java b/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java index 68002bde..4aa78a33 100644 --- a/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java +++ b/src/main/java/org/springframework/cloud/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2015 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. @@ -39,12 +39,15 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.crypto.codec.Base64; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestOperations; import org.springframework.security.oauth2.client.OAuth2RestTemplate; @@ -238,6 +241,8 @@ public UserInfoTokenServices userInfoTokenServices() { @Slf4j protected static class JwtTokenServicesConfiguration { + private RestTemplate keyUriRestTemplate = new RestTemplate(); + @Autowired private ResourceServerProperties resource; @@ -264,18 +269,23 @@ public JwtAccessTokenConverter jwtTokenEnhancer() { String keyValue = resource.getJwt().getKeyValue(); if (!StringUtils.hasText(keyValue)) { try { - keyValue = (String) new RestTemplate().getForObject( - resource.getJwt().getKeyUri(), Map.class).get("value"); - } - catch (ResourceAccessException e) { + HttpHeaders headers = new HttpHeaders(); + if (resource.getClientId() != null && resource.getClientSecret() != null) { + byte[] token = Base64.encode((resource.getClientId() + ":" + resource.getClientSecret()) + .getBytes()); + headers.add("Authorization", "Basic " + new String(token)); + } + HttpEntity requestEntity = new HttpEntity(headers); + keyValue = (String) keyUriRestTemplate.exchange( + resource.getJwt().getKeyUri(), HttpMethod.GET, requestEntity, Map.class).getBody() + .get("value"); + } catch (ResourceAccessException e) { // ignore log.warn("Failed to fetch token key (you may need to refresh when the auth server is back)"); } } - else { - if (StringUtils.hasText(keyValue) && !keyValue.startsWith("-----BEGIN")) { - converter.setSigningKey(keyValue); - } + if (StringUtils.hasText(keyValue) && !keyValue.startsWith("-----BEGIN")) { + converter.setSigningKey(keyValue); } if (keyValue != null) { converter.setVerifierKey(keyValue); diff --git a/src/test/java/org/springframework/cloud/security/oauth2/resource/JwtTokenServicesConfigurationTests.java b/src/test/java/org/springframework/cloud/security/oauth2/resource/JwtTokenServicesConfigurationTests.java new file mode 100644 index 00000000..a3d82c4c --- /dev/null +++ b/src/test/java/org/springframework/cloud/security/oauth2/resource/JwtTokenServicesConfigurationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2015 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.cloud.security.oauth2.resource; + +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.util.ReflectionUtils.findField; +import static org.springframework.util.ReflectionUtils.getField; +import static org.springframework.util.ReflectionUtils.makeAccessible; + +import java.lang.reflect.Field; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.cloud.security.oauth2.resource.ResourceServerTokenServicesConfiguration.JwtTokenServicesConfiguration; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.match.MockRestRequestMatchers; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.client.RestTemplate; + +/** + * @author Will Tran + * + */ +public class JwtTokenServicesConfigurationTests { + + @Test + public void testSymmetricKeyUri() throws Exception { + testKeyUri(symmetricTokenKeyResponse, "tokenkey"); + } + + @Test + public void testAsymmetricKeyUri() throws Exception { + testKeyUri(asymmetricTokenKeyResponse, asymmetricTokenKey); + } + + public void testKeyUri(String tokenKeyResponse, String keyValue) throws Exception { + JwtTokenServicesConfiguration config = new JwtTokenServicesConfiguration(); + String clientId = "clientId"; + String clientSecret = "clientSecret"; + String authHeaderValue = "Basic " + new String(Base64.encode((clientId + ":" + clientSecret).getBytes())); + String keyUri = "https://example.com/token_key"; + + ResourceServerProperties properties = new ResourceServerProperties(clientId, clientSecret); + properties.getJwt().setKeyUri(keyUri); + Field resourceServerPropertiesField = findField(JwtTokenServicesConfiguration.class, "resource"); + makeAccessible(resourceServerPropertiesField); + ReflectionUtils.setField(resourceServerPropertiesField, config, properties); + + Field restTemplateField = findField(JwtTokenServicesConfiguration.class, "keyUriRestTemplate"); + makeAccessible(restTemplateField); + RestTemplate restTemplate = (RestTemplate) getField(restTemplateField, config); + + MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); + mockServer + .expect(requestTo(keyUri)) + .andExpect(method(GET)) + .andExpect(MockRestRequestMatchers.header("Authorization", authHeaderValue)) + .andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(tokenKeyResponse)); + + JwtAccessTokenConverter converter = config.jwtTokenEnhancer(); + converter.afterPropertiesSet(); + + mockServer.verify(); + Assert.assertEquals(keyValue, converter.getKey().get("value")); + } + + private static String symmetricTokenKeyResponse = "{\"alg\":\"HMACSHA256\",\"value\":\"tokenkey\",\"kty\":\"MAC\",\"use\":\"sig\"}"; + private static String asymmetricTokenKeyResponse = "{\"alg\":\"SHA256withRSA\",\"value\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\\nrn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\\nfYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\\nLCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\\nkqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\\njfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\\nJwIDAQAB\\n-----END PUBLIC KEY-----\\n\",\"kty\":\"RSA\",\"use\":\"sig\",\"n\":\"ANJufZdrvYg5zG61x36pDq59nVUN73wSanA7hVCtN3ftT2Rm1ZTQqp5KSCfLMhaaVvJY51sHj+/i4lqUaM9CO32G93fE44VfOmPfexZeAwa8YDOikyTrhP7sZ6A4WUNeC4DlNnJF4zsznU7JxjCkASwpdL6XFwbRSzGkm6b9aM4vIewyclWehJxUGVFhnYEzIQ65qnr38feVP9enOVgQzpKsCJ+xpa8vZ/UrscoG3/IOQM6VnLrGYAyyCGeyU1JXQW/KlNmtA5eJry2Tp+MD6I34/QsNkCArHOfj8H9tXz/oc3/tVkkR252L/Lmp0TtIGfHpBmoITP9h+oKiW6NpyCc=\",\"e\":\"AQAB\"}"; + private static String asymmetricTokenKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\nrn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\nfYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\nLCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\nkqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\njfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\nJwIDAQAB\n-----END PUBLIC KEY-----\n"; + +}