diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 679e9e72..7dc71899 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -1146,6 +1146,95 @@ myrandom=${cachedrandom.appname.value} ---- ==== +[[spring-cloud-security]] +== Security + +[[spring-cloud-security-single-sign-on]] +=== Single Sign On + +NOTE: All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/[Spring Boot user guide]. + +[[spring-cloud-security-client-token-relay]] +==== Client Token Relay + +If your app is a user facing OAuth2 client (i.e. has declared +`@EnableOAuth2Sso` or `@EnableOAuth2Client`) then it has an +`OAuth2ClientContext` in request scope from Spring Boot. You can +create your own `OAuth2RestTemplate` from this context and an +autowired `OAuth2ProtectedResourceDetails`, and then the context will +always forward the access token downstream, also refreshing the access +token automatically if it expires. (These are features of Spring +Security and Spring Boot.) + +[[spring-cloud-security-resource-server-token-relay]] +==== Resource Server Token Relay + +If your app has `@EnableResourceServer` you might want to relay the +incoming token downstream to other services. If you use a +`RestTemplate` to contact the downstream services then this is just a +matter of how to create the template with the right context. + +If your service uses `UserInfoTokenServices` to authenticate incoming +tokens (i.e. it is using the `security.oauth2.user-info-uri` +configuration), then you can simply create an `OAuth2RestTemplate` +using an autowired `OAuth2ClientContext` (it will be populated by the +authentication process before it hits the backend code). Equivalently +(with Spring Boot 1.4), you could inject a +`UserInfoRestTemplateFactory` and grab its `OAuth2RestTemplate` in +your configuration. For example: + +.MyConfiguration.java +[source,java] +---- +@Bean +public OAuth2RestTemplate restTemplate(UserInfoRestTemplateFactory factory) { + return factory.getUserInfoRestTemplate(); +} +---- + +This rest template will then have the same `OAuth2ClientContext` +(request-scoped) that is used by the authentication filter, so you can +use it to send requests with the same access token. + +If your app is not using `UserInfoTokenServices` but is still a client +(i.e. it declares `@EnableOAuth2Client` or `@EnableOAuth2Sso`), then +with Spring Security Cloud any `OAuth2RestOperations` that the user +creates from an `@Autowired` `OAuth2Context` will also forward +tokens. This feature is implemented by default as an MVC handler +interceptor, so it only works in Spring MVC. If you are not using MVC +you could use a custom filter or AOP interceptor wrapping an +`AccessTokenContextRelay` to provide the same feature. + +Here's a basic +example showing the use of an autowired rest template created +elsewhere ("foo.com" is a Resource Server accepting the same tokens as +the surrounding app): + +.MyController.java +[source,java] +---- +@Autowired +private OAuth2RestOperations restTemplate; + +@RequestMapping("/relay") +public String relay() { + ResponseEntity response = + restTemplate.getForEntity("https://foo.com/bar", String.class); + return "Success! (" + response.getBody() + ")"; +} +---- + +If you don't want to forward tokens (and that is a valid +choice, since you might want to act as yourself, rather than the +client that sent you the token), then you only need to create your own +`OAuth2Context` instead of autowiring the default one. + +Feign clients will also pick up an interceptor that uses the +`OAuth2ClientContext` if it is available, so they should also do a +token relay anywhere where a `RestTemplate` would. + == Configuration Properties To see the list of all Spring Cloud Commons related configuration properties please check link:appendix.html[the Appendix page]. diff --git a/pom.xml b/pom.xml index 42881250..2d9dd48b 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ commons 1.0.0 + 2.1.2.RELEASE @@ -150,6 +151,11 @@ spring-cloud-test-support ${project.version} + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + ${spring-security-oauth2-autoconfigure.version} + diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index 3b78f158..52100722 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -108,6 +108,11 @@ spring-boot-starter-hateoas true + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + true + org.springframework.hateoas diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/security/AccessTokenContextRelay.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/security/AccessTokenContextRelay.java new file mode 100644 index 00000000..57af2718 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/security/AccessTokenContextRelay.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-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 + * + * 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.cloud.commons.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; + +/** + * Convenience class for relaying an access token from the {@link SecurityContext} to the + * {@link OAuth2ClientContext}. If successful then subsequent calls to an + * {@link OAuth2RestTemplate} using the context contained here will use the same access + * token. This is mostly useful for relaying calls to a resource server downstream to + * other resource servers. If the access token expires there is no way to refresh it, so + * expect an exception from downstream (propagating it to the caller is the best strategy, + * so they can refresh it and try again). + * + * @author Dave Syer + * + */ +public class AccessTokenContextRelay { + + private OAuth2ClientContext context; + + public AccessTokenContextRelay(OAuth2ClientContext context) { + this.context = context; + } + + /** + * Attempt to copy an access token from the security context into the oauth2 context. + * @return true if the token was copied + */ + public boolean copyToken() { + if (context.getAccessToken() == null) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object details = authentication.getDetails(); + if (details instanceof OAuth2AuthenticationDetails) { + OAuth2AuthenticationDetails holder = (OAuth2AuthenticationDetails) details; + String token = holder.getTokenValue(); + DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(token); + String tokenType = holder.getTokenType(); + if (tokenType != null) { + accessToken.setTokenType(tokenType); + } + context.setAccessToken(accessToken); + return true; + } + } + } + return false; + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/security/ResourceServerTokenRelayAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/security/ResourceServerTokenRelayAutoConfiguration.java new file mode 100644 index 00000000..e15b787a --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/security/ResourceServerTokenRelayAutoConfiguration.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-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 + * + * 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.cloud.commons.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +/** + * Adds an MVC interceptor for relaying OAuth2 access tokens into the client context (if + * there is one). In this way an incoming request to a resource server can be relayed + * downstream just be using @EnableOAuth2Client and an + * OAuth2RestTemplate. An MVC interceptor is used so as to have a minimal + * impact on the call stack. If you are not using MVC you could use a custom filter or AOP + * interceptor wrapping the same call to an {@link AccessTokenContextRelay}. + * + *
+ * + * N.B. an app that is using {@link UserInfoTokenServices} generally doesn't need this + * interceptor, but it doesn't hurt to include it. + * + * @author Dave Syer + * + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(OAuth2AutoConfiguration.class) +@ResourceServerTokenRelayAutoConfiguration.ConditionalOnOAuth2ClientInResourceServer +@ConditionalOnClass(ResourceServerConfiguration.class) +@ConditionalOnWebApplication +@ConditionalOnProperty(value = "spring.cloud.mvc.token-relay.enabled=true", matchIfMissing = true) +public class ResourceServerTokenRelayAutoConfiguration { + + @Bean + public AccessTokenContextRelay accessTokenContextRelay(OAuth2ClientContext context) { + return new AccessTokenContextRelay(context); + } + + /** + * A {@link WebMvcConfigurer} for the access token interceptor. + * + * @author Dave Syer + * + */ + @Configuration(proxyBeanMethods = false) + public static class ResourceServerTokenRelayRegistrationAutoConfiguration implements WebMvcConfigurer { + + @Autowired + AccessTokenContextRelay accessTokenContextRelay; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor( + + new HandlerInterceptorAdapter() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + accessTokenContextRelay.copyToken(); + return true; + } + } + + ); + } + + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Conditional(OAuth2OnClientInResourceServerCondition.class) + @interface ConditionalOnOAuth2ClientInResourceServer { + + } + + private static class OAuth2OnClientInResourceServerCondition extends AllNestedConditions { + + OAuth2OnClientInResourceServerCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(ResourceServerConfiguration.class) + static class Server { + + } + + @ConditionalOnBean(OAuth2ClientConfiguration.class) + static class Client { + + } + + } + +} diff --git a/spring-cloud-commons/src/main/resources/META-INF/spring.factories b/spring-cloud-commons/src/main/resources/META-INF/spring.factories index 92505625..016abdd6 100644 --- a/spring-cloud-commons/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-commons/src/main/resources/META-INF/spring.factories @@ -15,7 +15,8 @@ org.springframework.cloud.client.serviceregistry.ServiceRegistryAutoConfiguratio org.springframework.cloud.commons.httpclient.HttpClientConfiguration,\ org.springframework.cloud.commons.util.UtilAutoConfiguration,\ org.springframework.cloud.configuration.CompatibilityVerifierAutoConfiguration,\ -org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration +org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration,\ +org.springframework.cloud.commons.security.ResourceServerTokenRelayAutoConfiguration # Environment Post Processors org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.cloud.client.HostInfoEnvironmentPostProcessor diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/security/ResourceServerTokenRelayAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/security/ResourceServerTokenRelayAutoConfigurationTests.java new file mode 100644 index 00000000..dfe1b5af --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/security/ResourceServerTokenRelayAutoConfigurationTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015-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 + * + * 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.cloud.commons.security; + +import org.junit.After; +import org.junit.Test; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; +import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * @author Dave Syer + * + */ +public class ResourceServerTokenRelayAutoConfigurationTests { + + private ConfigurableApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void clientNotConfigured() { + this.context = new SpringApplicationBuilder(NoClientConfiguration.class) + .properties("spring.config.name=test", "server.port=0", "spring.cloud.mvc.token-relay.enabled=true", + "security.oauth2.resource.userInfoUri:https://example.com") + .run(); + assertThat(this.context.containsBean("loadBalancedOauth2RestTemplate")).isFalse(); + } + + @Test + public void clientConfigured() throws Exception { + this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test", + "server.port=0", "security.oauth2.resource.userInfoUri:https://example.com", + "security.oauth2.client.clientId=foo").run(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest())); + OAuth2ClientContext client = this.context.getBean(OAuth2ClientContext.class); + assertThat(client.getAccessToken()).isNull(); + UserInfoTokenServices services = context.getBean(UserInfoTokenServices.class); + OAuth2RestTemplate template = (OAuth2RestTemplate) ReflectionTestUtils.getField(services, "restTemplate"); + MockRestServiceServer server = MockRestServiceServer.createServer(template); + server.expect(requestTo("https://example.com")) + .andRespond(withSuccess("{\"id\":\"user\"}", MediaType.APPLICATION_JSON)); + services.loadAuthentication("FOO"); + assertThat(client.getAccessToken().getValue()).isEqualTo("FOO"); + server.verify(); + } + + @EnableAutoConfiguration + @Configuration(proxyBeanMethods = false) + @EnableResourceServer + protected static class NoClientConfiguration { + + } + + @EnableAutoConfiguration + @Configuration(proxyBeanMethods = false) + @EnableResourceServer + @EnableOAuth2Sso + protected static class ClientConfiguration { + + @Bean + public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource, + OAuth2ClientContext oauth2Context) { + return new OAuth2RestTemplate(resource, oauth2Context); + } + + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/security/tokenrelay/ResourceServerTokenRelayTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/security/tokenrelay/ResourceServerTokenRelayTests.java new file mode 100644 index 00000000..52913d86 --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/commons/security/tokenrelay/ResourceServerTokenRelayTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2015-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 + * + * 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.cloud.commons.security.tokenrelay; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.commons.security.AccessTokenContextRelay; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * @author Peter Szanto (spring@szantocsalad.hu) + * + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "security.oauth2.resource.jwt.keyValue=secret", "spring.cloud.mvc.token-relay.enabled=true", + "spring.autoconfigure.exclude=" }) +public class ResourceServerTokenRelayTests { + + protected static final String TOKEN_VALID_UNTIL_2085 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJleHAiOjM2NDA2ODU4ODIsInVzZXJfbmFtZSI6InJlYWRlciIsImF1dGhvcml0aWVzIjpbIlJPTEVfUkVBREVSIl0s" + + "Imp0aSI6ImRkOTAzZGM2LTI0NDctNDViMi04MDZjLTIzZjU3ODVhNGQ4MCIsImNsaWVudF9pZCI6IndlYi1hcHAiLCJzY29wZSI6WyJyZWFkIl19." + + "6hoNtxmN1_o5Ki0D0ae4amSOTRmit3pmaqv-z1-Qk4Y"; + + protected static final String AUTH_HEADER_TO_BE_RELAYED = "Bearer " + TOKEN_VALID_UNTIL_2085; + + protected static final String TEST_RESPONSE = "[\"test response\"]"; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Autowired + private MockRestServiceServer mockServerToReceiveRelay; + + @SpyBean + AccessTokenContextRelay accessTokenContextRelay; + + @Test + public void tokenRelayJWT() throws Exception { + + mockServerToReceiveRelay.expect(requestTo("https://example.com/test")) + .andExpect(header("authorization", AUTH_HEADER_TO_BE_RELAYED)) + .andRespond(withSuccess(TEST_RESPONSE, MediaType.APPLICATION_JSON)); + + HttpEntity authorizationHeader = createAuthorizationHeader(); + ResponseEntity exchange = testRestTemplate.exchange("/token-relay", HttpMethod.GET, authorizationHeader, + String.class); + + assertThat(exchange.getStatusCodeValue()).isEqualTo(HttpStatus.OK.value()); + assertThat(exchange.getBody()).isEqualTo(TEST_RESPONSE); + + mockServerToReceiveRelay.verify(); + verify(accessTokenContextRelay).copyToken(); + } + + private HttpEntity createAuthorizationHeader() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", AUTH_HEADER_TO_BE_RELAYED); + return new HttpEntity("parameters", headers); + + } + + @SpringBootApplication + @TestConfiguration + @EnableResourceServer + @ComponentScan(basePackageClasses = TokenRelayTestController.class) + @EnableOAuth2Client + protected static class ClientConfiguration { + + @Bean + public OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource, + OAuth2ClientContext oauth2Context) { + return new OAuth2RestTemplate(resource, oauth2Context); + + } + + @Bean + public MockRestServiceServer mockRestServiceServer(OAuth2RestTemplate template) { + return MockRestServiceServer.createServer(template); + } + + } + + @RestController + @TestComponent + protected static class TokenRelayTestController { + + @Autowired + OAuth2RestTemplate oAuth2RestTemplate; + + @GetMapping("/token-relay") + public String callAnotherService() { + + return oAuth2RestTemplate.getForEntity("https://example.com/test", String.class).getBody(); + + } + + } + +} diff --git a/spring-cloud-commons/src/test/resources/application.properties b/spring-cloud-commons/src/test/resources/application.properties index a3c02545..274d77aa 100644 --- a/spring-cloud-commons/src/test/resources/application.properties +++ b/spring-cloud-commons/src/test/resources/application.properties @@ -6,4 +6,7 @@ debug:true spring.cloud.hypermedia.refresh.initial-delay=50000 spring.cloud.hypermedia.refresh.fixed-delay=10000 management.security.enabled=false - +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration,org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration +security.oauth2.resource.tokenRelay=false +security.oauth2.client.clientId: acme +security.oauth2.client.clientSecret: acmesecret diff --git a/spring-cloud-loadbalancer/pom.xml b/spring-cloud-loadbalancer/pom.xml index 76529410..fccc3149 100644 --- a/spring-cloud-loadbalancer/pom.xml +++ b/spring-cloud-loadbalancer/pom.xml @@ -82,11 +82,21 @@ spring-retry true
+ + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + true + org.springframework.boot spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-web + test + org.springframework.cloud spring-cloud-test-support diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfiguration.java new file mode 100644 index 00000000..7b9972b3 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfiguration.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-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 + * + * 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.cloud.loadbalancer.security; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateCustomizer; +import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; +import org.springframework.cloud.client.loadbalancer.RetryLoadBalancerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; + +/** + * @author Dave Syer + * + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(OAuth2RestTemplate.class) +@ConditionalOnProperty("spring.cloud.oauth2.load-balanced.enabled=true") +@AutoConfigureAfter(OAuth2AutoConfiguration.class) +public class OAuth2LoadBalancerClientAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(LoadBalancerInterceptor.class) + protected static class UserInfoLoadBalancerConfig { + + @Bean + public UserInfoRestTemplateCustomizer loadBalancedUserInfoRestTemplateCustomizer( + final LoadBalancerInterceptor loadBalancerInterceptor) { + return new UserInfoRestTemplateCustomizer() { + @Override + public void customize(OAuth2RestTemplate restTemplate) { + List interceptors = new ArrayList<>(restTemplate.getInterceptors()); + interceptors.add(loadBalancerInterceptor); + restTemplate.setInterceptors(interceptors); + } + }; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(RetryLoadBalancerInterceptor.class) + protected static class UserInfoRetryLoadBalancerConfig { + + @Bean + public UserInfoRestTemplateCustomizer retryLoadBalancedUserInfoRestTemplateCustomizer( + final RetryLoadBalancerInterceptor loadBalancerInterceptor) { + return new UserInfoRestTemplateCustomizer() { + @Override + public void customize(OAuth2RestTemplate restTemplate) { + List interceptors = new ArrayList<>(restTemplate.getInterceptors()); + interceptors.add(loadBalancerInterceptor); + restTemplate.setInterceptors(interceptors); + } + }; + } + + } + +} diff --git a/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories index 3e9c647b..da847ea3 100644 --- a/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-loadbalancer/src/main/resources/META-INF/spring.factories @@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration,\ org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration,\ -org.springframework.cloud.loadbalancer.config.LoadBalancerCacheAutoConfiguration \ No newline at end of file +org.springframework.cloud.loadbalancer.config.LoadBalancerCacheAutoConfiguration,\ +org.springframework.cloud.loadbalancer.security.OAuth2LoadBalancerClientAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfigurationTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfigurationTests.java new file mode 100644 index 00000000..9bcb0e99 --- /dev/null +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/security/OAuth2LoadBalancerClientAutoConfigurationTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2015-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 + * + * 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.cloud.loadbalancer.security; + +import java.net.URI; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; +import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateFactory; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.test.ClassPathExclusions; +import org.springframework.cloud.test.ModifiedClassPathRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +@RunWith(ModifiedClassPathRunner.class) +@ClassPathExclusions("spring-retry-*.jar") +public class OAuth2LoadBalancerClientAutoConfigurationTests { + + private ConfigurableApplicationContext context; + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void userInfoNotLoadBalanced() { + this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test", + "server.port=0", "security.oauth2.resource.userInfoUri:https://example.com").run(); + + assertThat(this.context.containsBean("loadBalancedUserInfoRestTemplateCustomizer")).isFalse(); + assertThat(this.context.containsBean("retryLoadBalancedUserInfoRestTemplateCustomizer")).isFalse(); + } + + @Test + public void userInfoLoadBalancedNoRetry() throws Exception { + this.context = new SpringApplicationBuilder(ClientConfiguration.class).properties("spring.config.name=test", + "server.port=0", "security.oauth2.resource.userInfoUri:https://nosuchservice", + "spring.cloud.oauth2.load-balanced.enabled=true").run(); + + assertThat(this.context.containsBean("loadBalancedUserInfoRestTemplateCustomizer")).isTrue(); + assertThat(this.context.containsBean("retryLoadBalancedUserInfoRestTemplateCustomizer")).isFalse(); + + OAuth2RestTemplate template = this.context.getBean(UserInfoRestTemplateFactory.class).getUserInfoRestTemplate(); + ClientHttpRequest request = template.getRequestFactory().createRequest(new URI("https://nosuchservice"), + HttpMethod.GET); + expected.expectMessage("No instances available for nosuchservice"); + request.execute(); + } + + @EnableAutoConfiguration + @Configuration(proxyBeanMethods = false) + @EnableOAuth2Sso + protected static class ClientConfiguration { + + } + +} diff --git a/spring-cloud-loadbalancer/src/test/resources/application.yml b/spring-cloud-loadbalancer/src/test/resources/application.yml index a6073b3a..103f868d 100644 --- a/spring-cloud-loadbalancer/src/test/resources/application.yml +++ b/spring-cloud-loadbalancer/src/test/resources/application.yml @@ -25,3 +25,7 @@ spring: uri: http://hhost - service-id: myservice uri: http://ihost + autoconfigure.exclude: + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration + - org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration + - org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration \ No newline at end of file