From 53f67a448f8ef7a47ca5c472c26e1c1a1ef19359 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Thu, 2 Oct 2014 12:13:01 -0500 Subject: [PATCH] Auto-configure Spring Security OAuth2 when detected on the classpath * Automatically spin up Authorization Server and Resource Server * Automatically configures method level security included OAuth2Expression handler * Wrote extensive unit tests verifying default behavior as well as the auto-configuration backing off when custom Authorization/Resource servers are included * Created org.springframework.boot.security.oauth2 subpackage to contain it * Can also disable either resource of authorization server completely with a single property for each * Print out the auto-generated secrets and other settings * Added spring-boot-sample-secure-oauth2 to provide a sample that can be run and poked with curl as well as some automated tests. * Make users ask for which servers to install by adding @Enable* * User has to @EnableGlobalMethodSecurity instead of using properties files Add Spring Security OAuth2 support to Spring Boot CLI * Triggered from either @EnableAuthorizationServer or @EnableResourceServer * Needs to have @EnableGlobalMethodSecurity to allow picking the annotation model. * By default, comes with import support for @PreAuthorize, @PreFilter, @PostAuthorize, and @PostFilter via a single start import * Also need import support for the enable annotations mentioned above. * Added extra test case and sample (oauth2.groovy) --- .gitignore | 1 + spring-boot-autoconfigure/pom.xml | 40 ++ .../Http401AuthenticationEntryPoint.java | 51 ++ .../oauth2/ClientCredentialsProperties.java | 56 ++ ...SpringSecurityOAuth2AutoConfiguration.java | 78 +++ ...rityOAuth2MethodSecurityConfiguration.java | 70 +++ ...Auth2AuthorizationServerConfiguration.java | 125 ++++ ...ringSecurityOAuth2ClientConfiguration.java | 146 +++++ .../resource/ResourceServerProperties.java | 229 +++++++ ...ourceServerTokenServicesConfiguration.java | 281 +++++++++ ...rityOAuth2ResourceServerConfiguration.java | 157 +++++ .../resource/SpringSocialTokenServices.java | 76 +++ .../resource/UserInfoTokenServices.java | 131 ++++ .../main/resources/META-INF/spring.factories | 1 + ...gSecurityOAuth2AutoConfigurationTests.java | 558 ++++++++++++++++++ .../ResourceServerPropertiesTests.java | 52 ++ ...ServerTokenServicesConfigurationTests.java | 171 ++++++ .../resource/UserInfoTokenServicesTests.java | 72 +++ spring-boot-cli/samples/oauth2.groovy | 15 + ...curityOAuth2CompilerAutoConfiguration.java | 58 ++ ...oot.cli.compiler.CompilerAutoConfiguration | 2 + .../boot/cli/SampleIntegrationTests.java | 12 + spring-boot-dependencies/pom.xml | 11 +- spring-boot-samples/pom.xml | 1 + .../spring-boot-sample-secure-oauth2/pom.xml | 56 ++ .../src/main/java/sample/Application.java | 110 ++++ .../src/main/java/sample/Flight.java | 101 ++++ .../main/java/sample/FlightRepository.java | 40 ++ .../src/main/resources/application.properties | 8 + .../src/main/resources/data-h2.sql | 4 + .../src/main/resources/templates/.gitignore | 0 .../test/java/sample/ApplicationTests.java | 145 +++++ 32 files changed, 2857 insertions(+), 1 deletion(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/Http401AuthenticationEntryPoint.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/ClientCredentialsProperties.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2MethodSecurityConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/authserver/SpringSecurityOAuth2AuthorizationServerConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SpringSecurityOAuth2ClientConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSecurityOAuth2ResourceServerConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSocialTokenServices.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfigurationTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesTests.java create mode 100644 spring-boot-cli/samples/oauth2.groovy create mode 100644 spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringSecurityOAuth2CompilerAutoConfiguration.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/pom.xml create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Application.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Flight.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/FlightRepository.java create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/data-h2.sql create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/templates/.gitignore create mode 100644 spring-boot-samples/spring-boot-sample-secure-oauth2/src/test/java/sample/ApplicationTests.java diff --git a/.gitignore b/.gitignore index b50e97daf4..67cbed117a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ overridedb.* *.jar .DS_Store .factorypath +data diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index 3cb4302ac8..94e4f24333 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -299,16 +299,34 @@ org.springframework.data spring-data-jpa true + + + jcl-over-slf4j + org.slf4j + + org.springframework.data spring-data-rest-webmvc true + + + jcl-over-slf4j + org.slf4j + + org.springframework.data spring-data-mongodb true + + + jcl-over-slf4j + org.slf4j + + org.springframework.data @@ -319,11 +337,23 @@ org.springframework.data spring-data-elasticsearch true + + + jcl-over-slf4j + org.slf4j + + org.springframework.data spring-data-solr true + + + jcl-over-slf4j + org.slf4j + + org.springframework.hateoas @@ -355,6 +385,16 @@ spring-security-config true + + org.springframework.security.oauth + spring-security-oauth2 + true + + + org.springframework.security + spring-security-jwt + true + org.springframework.amqp spring-rabbit diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/Http401AuthenticationEntryPoint.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/Http401AuthenticationEntryPoint.java new file mode 100644 index 0000000000..f8bbbccb31 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/Http401AuthenticationEntryPoint.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; + +/** + * AuthenticationEntryPoint that sends a 401 and Parameterized by the value of the + * WWW-Authenticate header. Like the {@link BasicAuthenticationEntryPoint} but more + * flexible. + * + * @author Dave Syer + * + */ +public class Http401AuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final String authenticateHeader; + + public Http401AuthenticationEntryPoint(String authenticateHeader) { + this.authenticateHeader = authenticateHeader; + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + response.setHeader("WWW-Authenticate", authenticateHeader); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + authException.getMessage()); + } +} \ No newline at end of file diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/ClientCredentialsProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/ClientCredentialsProperties.java new file mode 100644 index 0000000000..30fd8d98ae --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/ClientCredentialsProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2; + +import java.util.UUID; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Dave Syer + */ +@ConfigurationProperties("spring.oauth2.client") +public class ClientCredentialsProperties { + + private String clientId; + + private String clientSecret = UUID.randomUUID().toString(); + + private boolean defaultSecret = true; + + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return this.clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + this.defaultSecret = false; + } + + public boolean isDefaultSecret() { + return this.defaultSecret; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfiguration.java new file mode 100644 index 0000000000..f4c325c5a8 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.oauth2.authserver.SpringSecurityOAuth2AuthorizationServerConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.SpringSecurityOAuth2ClientConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.SpringSecurityOAuth2ResourceServerConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * Spring Security OAuth2 top level auto-configuration beans + * + * @author Greg Turnquist + * @author Dave Syer + */ +@Configuration +@ConditionalOnClass({ OAuth2AccessToken.class, WebMvcConfigurerAdapter.class }) +@ConditionalOnWebApplication +@Import({ SpringSecurityOAuth2AuthorizationServerConfiguration.class, + SpringSecurityOAuth2MethodSecurityConfiguration.class, + SpringSecurityOAuth2ResourceServerConfiguration.class, + SpringSecurityOAuth2ClientConfiguration.class }) +@AutoConfigureBefore(WebMvcAutoConfiguration.class) +@EnableConfigurationProperties(ClientCredentialsProperties.class) +public class SpringSecurityOAuth2AutoConfiguration { + + @Configuration + protected static class ResourceServerOrderProcessor implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof ResourceServerConfiguration) { + ResourceServerConfiguration configuration = (ResourceServerConfiguration) bean; + configuration.setOrder(getOrder()); + } + return bean; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + private int getOrder() { + // Before the authorization server (default 0) + return -10; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2MethodSecurityConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2MethodSecurityConfiguration.java new file mode 100644 index 0000000000..7510702a37 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2MethodSecurityConfiguration.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler; + +/** + * Auto-configure an expression handler for method-level security (if the user already has + * @EnableGlobalMethodSecurity). + * + * @author Greg Turnquist + * @author Dave Syer + */ +@Configuration +@ConditionalOnClass({ OAuth2AccessToken.class }) +@ConditionalOnBean(GlobalMethodSecurityConfiguration.class) +public class SpringSecurityOAuth2MethodSecurityConfiguration implements + BeanFactoryPostProcessor { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + beanFactory + .addBeanPostProcessor(new OAuth2ExpressionHandlerInjectionPostProcessor()); + } + + private static class OAuth2ExpressionHandlerInjectionPostProcessor implements + BeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof DefaultMethodSecurityExpressionHandler) { + return new OAuth2MethodSecurityExpressionHandler(); + } + return bean; + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/authserver/SpringSecurityOAuth2AuthorizationServerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/authserver/SpringSecurityOAuth2AuthorizationServerConfiguration.java new file mode 100644 index 0000000000..0d0aa21d9f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/authserver/SpringSecurityOAuth2AuthorizationServerConfiguration.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.authserver; + +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.oauth2.ClientCredentialsProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder; +import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.security.oauth2.provider.token.TokenStore; + +/** + * Auto-configure a Spring Security OAuth2 authorization server. Back off if another + * {@link AuthorizationServerConfigurer} already exists or if authorization server is not + * enabled. + * + * @author Greg Turnquist + * @author Dave Syer + */ +@Configuration +@ConditionalOnClass(EnableAuthorizationServer.class) +@ConditionalOnMissingBean(AuthorizationServerConfigurer.class) +@ConditionalOnBean(AuthorizationServerEndpointsConfiguration.class) +@EnableConfigurationProperties +public class SpringSecurityOAuth2AuthorizationServerConfiguration extends + AuthorizationServerConfigurerAdapter { + + @Autowired + private BaseClientDetails details; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired(required = false) + private TokenStore tokenStore; + + @Configuration + @ConditionalOnMissingBean(BaseClientDetails.class) + protected static class BaseClientDetailsConfiguration { + + @Autowired + private ClientCredentialsProperties client; + + @Bean + @ConfigurationProperties("spring.oauth2.client") + public BaseClientDetails oauth2ClientDetails() { + BaseClientDetails details = new BaseClientDetails(); + if (this.client.getClientId() == null) { + this.client.setClientId(UUID.randomUUID().toString()); + } + details.setClientId(this.client.getClientId()); + details.setClientSecret(this.client.getClientSecret()); + details.setAuthorizedGrantTypes(Arrays.asList("authorization_code", + "password", "client_credentials", "implicit", "refresh_token")); + details.setAuthorities(AuthorityUtils + .commaSeparatedStringToAuthorityList("ROLE_USER")); + details.setRegisteredRedirectUri(Collections. emptySet()); + return details; + } + + } + + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + ClientDetailsServiceBuilder.ClientBuilder builder = clients + .inMemory().withClient(this.details.getClientId()); + builder.secret(this.details.getClientSecret()) + .resourceIds(this.details.getResourceIds().toArray(new String[0])) + .authorizedGrantTypes( + this.details.getAuthorizedGrantTypes().toArray(new String[0])) + .authorities( + AuthorityUtils.authorityListToSet(this.details.getAuthorities()) + .toArray(new String[0])) + .scopes(this.details.getScope().toArray(new String[0])); + if (this.details.getRegisteredRedirectUri() != null) { + builder.redirectUris(this.details.getRegisteredRedirectUri().toArray( + new String[0])); + } + } + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) + throws Exception { + if (this.tokenStore != null) { + endpoints.tokenStore(this.tokenStore); + } + if (this.details.getAuthorizedGrantTypes().contains("password")) { + endpoints.authenticationManager(this.authenticationManager); + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SpringSecurityOAuth2ClientConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SpringSecurityOAuth2ClientConfiguration.java new file mode 100644 index 0000000000..88000cccb0 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SpringSecurityOAuth2ClientConfiguration.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.client; + +import java.io.IOException; +import java.util.Arrays; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.security.oauth2.ClientCredentialsProperties; +import org.springframework.boot.context.embedded.FilterRegistrationBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.http.HttpHeaders; +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.oauth2.client.DefaultOAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.client.token.AccessTokenRequest; +import org.springframework.security.oauth2.client.token.RequestEnhancer; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; +import org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration; +import org.springframework.util.MultiValueMap; + +/** + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnClass(EnableOAuth2Client.class) +@ConditionalOnBean(OAuth2ClientConfiguration.class) +public class SpringSecurityOAuth2ClientConfiguration { + + private static final Log logger = LogFactory + .getLog(SpringSecurityOAuth2ClientConfiguration.class); + + @Configuration + public static class ClientAuthenticationFilterConfiguration { + + @Resource + @Qualifier("accessTokenRequest") + private AccessTokenRequest accessTokenRequest; + + @Autowired + private ClientCredentialsProperties credentials; + + @PostConstruct + public void init() { + String prefix = "spring.oauth2.client"; + boolean defaultSecret = this.credentials.isDefaultSecret(); + logger.info(String.format( + "Initialized OAuth2 Client\n\n%s.clientId = %s\n%s.secret = %s\n\n", + prefix, this.credentials.getClientId(), prefix, + defaultSecret ? this.credentials.getClientSecret() : "****")); + } + + @Bean + @ConfigurationProperties("spring.oauth2.client") + @Primary + public AuthorizationCodeResourceDetails authorizationCodeResourceDetails() { + AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); + details.setClientSecret(this.credentials.getClientSecret()); + details.setClientId(this.credentials.getClientId()); + return details; + } + + @Bean + public FilterRegistrationBean oauth2ClientFilterRegistration( + OAuth2ClientContextFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(filter); + registration.setOrder(0); + return registration; + } + + @Bean + public OAuth2RestOperations authorizationCodeRestTemplate( + AuthorizationCodeResourceDetails oauth2RemoteResource) { + OAuth2RestTemplate template = new OAuth2RestTemplate(oauth2RemoteResource, + oauth2ClientContext()); + template.setInterceptors(Arrays + . asList(new ClientHttpRequestInterceptor() { + @Override + public ClientHttpResponse intercept(HttpRequest request, + byte[] body, ClientHttpRequestExecution execution) + throws IOException { + request.getHeaders().setAccept( + Arrays.asList(MediaType.APPLICATION_JSON)); + return execution.execute(request, body); + } + })); + AuthorizationCodeAccessTokenProvider accessTokenProvider = new AuthorizationCodeAccessTokenProvider(); + accessTokenProvider.setTokenRequestEnhancer(new RequestEnhancer() { + @Override + public void enhance(AccessTokenRequest request, + OAuth2ProtectedResourceDetails resource, + MultiValueMap form, HttpHeaders headers) { + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + } + }); + template.setAccessTokenProvider(accessTokenProvider); + return template; + } + + @Bean + @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES) + public OAuth2ClientContext oauth2ClientContext() { + return new DefaultOAuth2ClientContext(this.accessTokenRequest); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java new file mode 100644 index 0000000000..149244f819 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java @@ -0,0 +1,229 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * @author Dave Syer + * + */ +@ConfigurationProperties("spring.oauth2.resource") +public class ResourceServerProperties implements Validator, BeanFactoryAware { + + @JsonIgnore + private final String clientId; + + @JsonIgnore + private final String clientSecret; + + @JsonIgnore + private ListableBeanFactory beanFactory; + + private String serviceId = "resource"; + + /** + * Identifier of the resource. + */ + private String id; + + /** + * URI of the user endpoint. + */ + private String userInfoUri; + + /** + * URI of the token decoding endpoint. + */ + private String tokenInfoUri; + + /** + * Use the token info, can be set to false to use the user info. + */ + private boolean preferTokenInfo = true; + + private Jwt jwt = new Jwt(); + + public ResourceServerProperties() { + this(null, null); + } + + public ResourceServerProperties(String clientId, String clientSecret) { + this.clientId = clientId; + this.clientSecret = clientSecret; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + public String getResourceId() { + return this.id; + } + + public String getServiceId() { + return this.serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserInfoUri() { + return this.userInfoUri; + } + + public void setUserInfoUri(String userInfoUri) { + this.userInfoUri = userInfoUri; + } + + public String getTokenInfoUri() { + return this.tokenInfoUri; + } + + public void setTokenInfoUri(String tokenInfoUri) { + this.tokenInfoUri = tokenInfoUri; + } + + public boolean isPreferTokenInfo() { + return this.preferTokenInfo; + } + + public void setPreferTokenInfo(boolean preferTokenInfo) { + this.preferTokenInfo = preferTokenInfo; + } + + public Jwt getJwt() { + return this.jwt; + } + + public void setJwt(Jwt jwt) { + this.jwt = jwt; + } + + public String getClientId() { + return this.clientId; + } + + public String getClientSecret() { + return this.clientSecret; + } + + @Override + public boolean supports(Class clazz) { + return ResourceServerProperties.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, + AuthorizationServerEndpointsConfiguration.class).length > 0) { + // If we are an authorization server we don't need remote resource token + // services + return; + } + ResourceServerProperties resource = (ResourceServerProperties) target; + if (StringUtils.hasText(this.clientId)) { + if (!StringUtils.hasText(this.clientSecret)) { + if (!StringUtils.hasText(resource.getUserInfoUri())) { + errors.rejectValue("userInfoUri", "missing.userInfoUri", + "Missing userInfoUri (no client secret available)"); + } + } + else { + if (isPreferTokenInfo() + && !StringUtils.hasText(resource.getTokenInfoUri())) { + if (StringUtils.hasText(getJwt().getKeyUri()) + || StringUtils.hasText(getJwt().getKeyValue())) { + // It's a JWT decoder + return; + } + if (!StringUtils.hasText(resource.getUserInfoUri())) { + errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri", + "Missing tokenInfoUri and userInfoUri and there is no JWT verifier key"); + } + } + } + } + } + + public class Jwt { + + /** + * The verification key of the JWT token. Can either be a symmetric secret or + * PEM-encoded RSA public key. If the value is not available, you can set the URI + * instead. + */ + private String keyValue; + + /** + * The URI of the JWT token. Can be set if the value is not available and the key + * is public. + */ + private String keyUri; + + public String getKeyValue() { + return this.keyValue; + } + + public void setKeyValue(String keyValue) { + this.keyValue = keyValue; + } + + public void setKeyUri(String keyUri) { + this.keyUri = keyUri; + } + + public String getKeyUri() { + if (this.keyUri != null) { + return this.keyUri; + } + if (ResourceServerProperties.this.userInfoUri != null + && ResourceServerProperties.this.userInfoUri.endsWith("/userinfo")) { + return ResourceServerProperties.this.userInfoUri.replace("/userinfo", + "/token_key"); + } + if (ResourceServerProperties.this.tokenInfoUri != null + && ResourceServerProperties.this.tokenInfoUri + .endsWith("/check_token")) { + return ResourceServerProperties.this.userInfoUri.replace("/check_token", + "/token_key"); + } + return null; + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java new file mode 100644 index 0000000000..f062635382 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java @@ -0,0 +1,281 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import java.util.Collections; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.security.oauth2.ClientCredentialsProperties; +import org.springframework.boot.autoconfigure.security.oauth2.client.SpringSecurityOAuth2ClientConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.SpringSecurityOAuth2ClientConfiguration.ClientAuthenticationFilterConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; +import org.springframework.security.oauth2.provider.token.DefaultTokenServices; +import org.springframework.security.oauth2.provider.token.RemoteTokenServices; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +import org.springframework.social.connect.ConnectionFactoryLocator; +import org.springframework.social.connect.support.OAuth2ConnectionFactory; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +/** + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnMissingBean(AuthorizationServerEndpointsConfiguration.class) +public class ResourceServerTokenServicesConfiguration { + + private static final Log logger = LogFactory + .getLog(ResourceServerTokenServicesConfiguration.class); + + @Configuration + @Conditional(NotJwtToken.class) + @EnableOAuth2Client + @Import(ClientAuthenticationFilterConfiguration.class) + protected static class RemoteTokenServicesConfiguration { + + @Configuration + @Import(SpringSecurityOAuth2ClientConfiguration.class) + @Conditional(TokenInfo.class) + protected static class TokenInfoServicesConfiguration { + + @Autowired + private ResourceServerProperties resource; + + @Autowired + private AuthorizationCodeResourceDetails client; + + @Bean + public ResourceServerTokenServices remoteTokenServices() { + RemoteTokenServices services = new RemoteTokenServices(); + services.setCheckTokenEndpointUrl(this.resource.getTokenInfoUri()); + services.setClientId(this.client.getClientId()); + services.setClientSecret(this.client.getClientSecret()); + return services; + } + + } + + @Configuration + @ConditionalOnClass(OAuth2ConnectionFactory.class) + @Conditional(NotTokenInfo.class) + protected static class SocialTokenServicesConfiguration { + + @Autowired + private ResourceServerProperties sso; + + @Autowired + private ClientCredentialsProperties client; + + @Autowired(required = false) + private OAuth2ConnectionFactory connectionFactory; + + @Autowired(required = false) + private Map resources = Collections.emptyMap(); + + @Bean + @ConditionalOnBean(ConnectionFactoryLocator.class) + @ConditionalOnMissingBean(ResourceServerTokenServices.class) + public SpringSocialTokenServices socialTokenServices() { + return new SpringSocialTokenServices(this.connectionFactory, + this.client.getClientId()); + } + + @Bean + @ConditionalOnMissingBean({ ConnectionFactoryLocator.class, + ResourceServerTokenServices.class }) + public ResourceServerTokenServices userInfoTokenServices() { + UserInfoTokenServices services = new UserInfoTokenServices( + this.sso.getUserInfoUri(), this.client.getClientId()); + services.setResources(this.resources); + return services; + } + + } + + @Configuration + @ConditionalOnMissingClass(name = "org.springframework.social.connect.support.OAuth2ConnectionFactory") + @Conditional(NotTokenInfo.class) + protected static class UserInfoTokenServicesConfiguration { + + @Autowired + private ResourceServerProperties sso; + + @Autowired + private ClientCredentialsProperties client; + + @Autowired(required = false) + private Map resources = Collections.emptyMap(); + + @Bean + @ConditionalOnMissingBean(ResourceServerTokenServices.class) + public ResourceServerTokenServices userInfoTokenServices() { + UserInfoTokenServices services = new UserInfoTokenServices( + this.sso.getUserInfoUri(), this.client.getClientId()); + services.setResources(this.resources); + return services; + } + + } + + } + + @Configuration + @Conditional(JwtToken.class) + protected static class JwtTokenServicesConfiguration { + + @Autowired + private ResourceServerProperties resource; + + @Bean + @ConditionalOnMissingBean(ResourceServerTokenServices.class) + public ResourceServerTokenServices jwtTokenServices() { + DefaultTokenServices services = new DefaultTokenServices(); + services.setTokenStore(jwtTokenStore()); + return services; + } + + @Bean + public TokenStore jwtTokenStore() { + return new JwtTokenStore(jwtTokenEnhancer()); + } + + @Bean + public JwtAccessTokenConverter jwtTokenEnhancer() { + JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); + String keyValue = this.resource.getJwt().getKeyValue(); + if (!StringUtils.hasText(keyValue)) { + try { + keyValue = (String) new RestTemplate().getForObject( + this.resource.getJwt().getKeyUri(), Map.class).get("value"); + } + catch (ResourceAccessException e) { + // ignore + logger.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 (keyValue != null) { + converter.setVerifierKey(keyValue); + } + return converter; + } + + } + + private static class TokenInfo extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + Environment environment = context.getEnvironment(); + boolean preferTokenInfo = environment + .resolvePlaceholders( + "${spring.oauth2.resource.preferTokenInfo:${OAUTH2_RESOURCE_PREFERTOKENINFO:true}}") + .equals("true"); + boolean hasTokenInfo = !environment.resolvePlaceholders( + "${spring.oauth2.resource.tokenInfoUri:}").equals(""); + boolean hasUserInfo = !environment.resolvePlaceholders( + "${spring.oauth2.resource.userInfoUri:}").equals(""); + if (!hasUserInfo) { + return ConditionOutcome.match("No user info provided"); + } + if (hasTokenInfo) { + if (preferTokenInfo) { + return ConditionOutcome + .match("Token info endpoint is preferred and user info provided"); + } + } + return ConditionOutcome.noMatch("Token info endpoint is not provided"); + } + + } + + private static class JwtToken extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + if (StringUtils.hasText(context.getEnvironment().getProperty( + "spring.oauth2.resource.jwt.keyValue")) + || StringUtils.hasText(context.getEnvironment().getProperty( + "spring.oauth2.resource.jwt.keyUri"))) { + return ConditionOutcome.match("public key is provided"); + } + return ConditionOutcome.noMatch("public key is not provided"); + } + + } + + private static class NotTokenInfo extends SpringBootCondition { + + private TokenInfo opposite = new TokenInfo(); + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + ConditionOutcome outcome = this.opposite.getMatchOutcome(context, metadata); + if (outcome.isMatch()) { + return ConditionOutcome.noMatch(outcome.getMessage()); + } + return ConditionOutcome.match(outcome.getMessage()); + } + + } + + private static class NotJwtToken extends SpringBootCondition { + + private JwtToken opposite = new JwtToken(); + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + ConditionOutcome outcome = this.opposite.getMatchOutcome(context, metadata); + if (outcome.isMatch()) { + return ConditionOutcome.noMatch(outcome.getMessage()); + } + return ConditionOutcome.match(outcome.getMessage()); + } + + } +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSecurityOAuth2ResourceServerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSecurityOAuth2ResourceServerConfiguration.java new file mode 100644 index 0000000000..985c8dda3a --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSecurityOAuth2ResourceServerConfiguration.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.OnBeanCondition; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.oauth2.ClientCredentialsProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.SpringSecurityOAuth2ResourceServerConfiguration.ResourceServerCondition; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Auto-configure a Spring Security OAuth2 resource server. Back off if another + * {@link ResourceServerConfigurer} already exists or if resource server not enabled. + * + * @author Greg Turnquist + * @author Dave Syer + */ +@Configuration +@Conditional(ResourceServerCondition.class) +@ConditionalOnClass({ EnableResourceServer.class, SecurityProperties.class }) +@ConditionalOnWebApplication +@ConditionalOnBean(ResourceServerConfiguration.class) +@Import(ResourceServerTokenServicesConfiguration.class) +public class SpringSecurityOAuth2ResourceServerConfiguration { + + @Autowired + private ResourceServerProperties resource; + + @Bean + @ConditionalOnMissingBean(ResourceServerConfigurer.class) + public ResourceServerConfigurer resourceServer() { + return new ResourceSecurityConfigurer(this.resource); + } + + @Configuration + protected static class ResourceServerPropertiesConfiguration { + + @Autowired + private ClientCredentialsProperties credentials; + + @Bean + public ResourceServerProperties resourceServerProperties() { + return new ResourceServerProperties(this.credentials.getClientId(), + this.credentials.getClientSecret()); + } + } + + protected static class ResourceSecurityConfigurer extends + ResourceServerConfigurerAdapter { + + private ResourceServerProperties resource; + + @Autowired + public ResourceSecurityConfigurer(ResourceServerProperties resource) { + this.resource = resource; + } + + @Override + public void configure(ResourceServerSecurityConfigurer resources) + throws Exception { + resources.resourceId(this.resource.getResourceId()); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated(); + } + + } + + @ConditionalOnBean(AuthorizationServerEndpointsConfiguration.class) + protected static class ResourceServerCondition extends SpringBootCondition implements + ConfigurationCondition { + + private OnBeanCondition condition = new OnBeanCondition(); + + private StandardAnnotationMetadata beanMetaData = new StandardAnnotationMetadata( + ResourceServerCondition.class); + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + Environment environment = context.getEnvironment(); + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment); + String client = environment + .resolvePlaceholders("${spring.oauth2.client.clientId:}"); + if (StringUtils.hasText(client)) { + return ConditionOutcome.match("found client id"); + } + if (!resolver.getSubProperties("spring.oauth2.resource.jwt").isEmpty()) { + return ConditionOutcome.match("found JWT resource configuration"); + } + if (StringUtils.hasText(resolver + .getProperty("spring.oauth2.resource.userInfoUri"))) { + return ConditionOutcome + .match("found UserInfo URI resource configuration"); + } + if (ClassUtils + .isPresent( + "org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration", + null)) { + if (this.condition.matches(context, this.beanMetaData)) { + return ConditionOutcome + .match("found authorization server configuration"); + } + } + return ConditionOutcome + .noMatch("found neither client id nor JWT resource nor authorization server"); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSocialTokenServices.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSocialTokenServices.java new file mode 100644 index 0000000000..170f73c38e --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/SpringSocialTokenServices.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.social.connect.Connection; +import org.springframework.social.connect.UserProfile; +import org.springframework.social.connect.support.OAuth2ConnectionFactory; +import org.springframework.social.oauth2.AccessGrant; + +/** + * @author Dave Syer + * + */ +public class SpringSocialTokenServices implements ResourceServerTokenServices { + + protected final Log logger = LogFactory.getLog(getClass()); + + private OAuth2ConnectionFactory connectionFactory; + + private String clientId; + + public SpringSocialTokenServices(OAuth2ConnectionFactory connectionFactory, + String clientId) { + this.connectionFactory = connectionFactory; + this.clientId = clientId; + } + + @Override + public OAuth2Authentication loadAuthentication(String accessToken) + throws AuthenticationException, InvalidTokenException { + + Connection connection = connectionFactory.createConnection(new AccessGrant( + accessToken)); + UserProfile user = connection.fetchUserProfile(); + return extractAuthentication(user); + } + + private OAuth2Authentication extractAuthentication(UserProfile user) { + UsernamePasswordAuthenticationToken principal = new UsernamePasswordAuthenticationToken( + user.getUsername(), "N/A", + AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); + principal.setDetails(user); + OAuth2Request request = new OAuth2Request(null, clientId, null, true, null, null, + null, null, null); + return new OAuth2Authentication(request, principal); + } + + @Override + public OAuth2AccessToken readAccessToken(String accessToken) { + throw new UnsupportedOperationException("Not supported: read access token"); + } + +} \ No newline at end of file diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java new file mode 100644 index 0000000000..c482eee0a0 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java @@ -0,0 +1,131 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; + +public class UserInfoTokenServices implements ResourceServerTokenServices { + + protected final Log logger = LogFactory.getLog(getClass()); + + private String userInfoEndpointUrl; + + private String clientId; + + private Collection resources = Collections.emptySet(); + + public UserInfoTokenServices(String userInfoEndpointUrl, String clientId) { + this.userInfoEndpointUrl = userInfoEndpointUrl; + this.clientId = clientId; + } + + public void setResources(Map resources) { + this.resources = new ArrayList(); + for (Entry key : resources.entrySet()) { + OAuth2RestOperations value = key.getValue(); + String clientIdForTemplate = value.getResource().getClientId(); + if (clientIdForTemplate!=null && clientIdForTemplate.equals(clientId)) { + this.resources.add(value); + } + } + } + + @Override + public OAuth2Authentication loadAuthentication(String accessToken) + throws AuthenticationException, InvalidTokenException { + + Map map = getMap(userInfoEndpointUrl, accessToken); + + if (map.containsKey("error")) { + logger.debug("userinfo returned error: " + map.get("error")); + throw new InvalidTokenException(accessToken); + } + + return extractAuthentication(map); + } + + private OAuth2Authentication extractAuthentication(Map map) { + UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken( + getPrincipal(map), "N/A", + AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); + user.setDetails(map); + OAuth2Request request = new OAuth2Request(null, clientId, null, true, null, null, + null, null, null); + return new OAuth2Authentication(request, user); + } + + private Object getPrincipal(Map map) { + String[] keys = new String[] { "user", "username", "userid", "user_id", "login", + "id" }; + for (String key : keys) { + if (map.containsKey(key)) { + return map.get(key); + } + } + return "unknown"; + } + + @Override + public OAuth2AccessToken readAccessToken(String accessToken) { + throw new UnsupportedOperationException("Not supported: read access token"); + } + + private Map getMap(String path, String accessToken) { + logger.info("Getting user info from: " + path); + OAuth2RestOperations restTemplate = null; + for (OAuth2RestOperations candidate : resources) { + try { + if (accessToken.equals(candidate.getAccessToken().getValue())) { + restTemplate = candidate; + } + } + catch (Exception e) { + } + } + if (restTemplate == null) { + BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); + resource.setClientId(clientId); + restTemplate = new OAuth2RestTemplate(resource); + restTemplate.getOAuth2ClientContext().setAccessToken( + new DefaultOAuth2AccessToken(accessToken)); + } + @SuppressWarnings("rawtypes") + Map map = restTemplate.getForEntity(path, Map.class).getBody(); + @SuppressWarnings("unchecked") + Map result = map; + return result; + } + +} \ No newline at end of file diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index aad5d81dbc..2f9a1d8018 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -52,6 +52,7 @@ org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration,\ org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.oauth2.SpringSecurityOAuth2AutoConfiguration,\ org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration,\ org.springframework.boot.autoconfigure.social.FacebookAutoConfiguration,\ org.springframework.boot.autoconfigure.social.LinkedInAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfigurationTests.java new file mode 100644 index 0000000000..dc0a8ab1fa --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/SpringSecurityOAuth2AutoConfigurationTests.java @@ -0,0 +1,558 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.authserver.SpringSecurityOAuth2AuthorizationServerConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.SpringSecurityOAuth2ResourceServerConfiguration; +import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; +import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource; +import org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource; +import org.springframework.security.access.method.MethodSecurityMetadataSource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.approval.ApprovalStore; +import org.springframework.security.oauth2.provider.approval.ApprovalStoreUserApprovalHandler; +import org.springframework.security.oauth2.provider.approval.TokenApprovalStore; +import org.springframework.security.oauth2.provider.approval.UserApprovalHandler; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService; +import org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint; +import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler; +import org.springframework.security.oauth2.provider.token.DefaultTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.JsonNode; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Verify Spring Security OAuth2 auto-configuration secures end points properly, accepts + * environmental overrides, and also backs off in the presence of other + * resource/authorization components. + * + * @author Greg Turnquist + * @author Dave Syer + */ +public class SpringSecurityOAuth2AutoConfigurationTests { + + private AnnotationConfigEmbeddedWebApplicationContext context; + + @Test + public void testDefaultConfiguration() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(AuthorizationAndResourceServerConfiguration.class, + MinimalSecureWebApplication.class); + this.context.refresh(); + + this.context.getBean(SpringSecurityOAuth2AuthorizationServerConfiguration.class); + this.context.getBean(SpringSecurityOAuth2ResourceServerConfiguration.class); + this.context.getBean(SpringSecurityOAuth2MethodSecurityConfiguration.class); + + ClientDetails config = this.context.getBean(BaseClientDetails.class); + AuthorizationEndpoint endpoint = this.context + .getBean(AuthorizationEndpoint.class); + UserApprovalHandler handler = (UserApprovalHandler) ReflectionTestUtils.getField( + endpoint, "userApprovalHandler"); + ClientDetailsService clientDetailsService = this.context + .getBean(ClientDetailsService.class); + ClientDetails clientDetails = clientDetailsService.loadClientByClientId(config + .getClientId()); + + assertThat(AopUtils.isJdkDynamicProxy(clientDetailsService), is(true)); + assertThat(AopUtils.getTargetClass(clientDetailsService).getName(), + is(ClientDetailsService.class.getName())); + + assertThat(handler instanceof ApprovalStoreUserApprovalHandler, is(true)); + + assertThat(clientDetails, equalTo(config)); + + verifyAuthentication(config); + } + + @Test + public void testEnvironmentalOverrides() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.oauth2.client.clientId:myclientid", + "spring.oauth2.client.clientSecret:mysecret"); + this.context.register(AuthorizationAndResourceServerConfiguration.class, + MinimalSecureWebApplication.class); + this.context.refresh(); + + ClientDetails config = this.context.getBean(ClientDetails.class); + + assertThat(config.getClientId(), is("myclientid")); + assertThat(config.getClientSecret(), is("mysecret")); + + verifyAuthentication(config); + } + + @Test + public void testDisablingResourceServer() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(AuthorizationServerConfiguration.class, + MinimalSecureWebApplication.class); + this.context.refresh(); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2ResourceServerConfiguration.class).length, + is(0)); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2AuthorizationServerConfiguration.class).length, + is(1)); + } + + @Test + public void testDisablingAuthorizationServer() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(ResourceServerConfiguration.class, + MinimalSecureWebApplication.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.oauth2.resource.jwt.keyValue:DEADBEEF"); + this.context.refresh(); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2ResourceServerConfiguration.class).length, + is(1)); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2AuthorizationServerConfiguration.class).length, + is(0)); + + assertThat(this.context.getBeanNamesForType(UserApprovalHandler.class).length, + is(0)); + assertThat(this.context.getBeanNamesForType(DefaultTokenServices.class).length, + is(1)); + } + + @Test + public void testResourceServerOverride() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(AuthorizationAndResourceServerConfiguration.class, + CustomResourceServer.class, MinimalSecureWebApplication.class); + this.context.refresh(); + + ClientDetails config = this.context.getBean(ClientDetails.class); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2AuthorizationServerConfiguration.class).length, + is(1)); + + assertThat(this.context.getBeanNamesForType(CustomResourceServer.class).length, + is(1)); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2ResourceServerConfiguration.class).length, + is(1)); + + verifyAuthentication(config); + + } + + @Test + public void testAuthorizationServerOverride() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.oauth2.resourceId:resource-id"); + this.context.register(AuthorizationAndResourceServerConfiguration.class, + CustomAuthorizationServer.class, MinimalSecureWebApplication.class); + this.context.refresh(); + + BaseClientDetails config = new BaseClientDetails(); + config.setClientId("client"); + config.setClientSecret("secret"); + config.setResourceIds(Arrays.asList("resource-id")); + config.setAuthorizedGrantTypes(Arrays.asList("password")); + config.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList("USER")); + config.setScope(Arrays.asList("read")); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2AuthorizationServerConfiguration.class).length, + is(0)); + + assertThat( + this.context + .getBeanNamesForType(SpringSecurityOAuth2ResourceServerConfiguration.class).length, + is(1)); + + verifyAuthentication(config); + } + + @Test + public void testDefaultPrePostSecurityAnnotations() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(AuthorizationAndResourceServerConfiguration.class, + MinimalSecureWebApplication.class); + this.context.refresh(); + + this.context.getBean(SpringSecurityOAuth2MethodSecurityConfiguration.class); + + ClientDetails config = this.context.getBean(ClientDetails.class); + + DelegatingMethodSecurityMetadataSource source = this.context + .getBean(DelegatingMethodSecurityMetadataSource.class); + List sources = source + .getMethodSecurityMetadataSources(); + + assertThat(sources.size(), is(1)); + assertThat(sources.get(0).getClass().getName(), + is(PrePostAnnotationSecurityMetadataSource.class.getName())); + + verifyAuthentication(config); + } + + @Test + public void testClassicSecurityAnnotationOverride() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(SecuredEnabledConfiguration.class, + MinimalSecureWebApplication.class); + this.context.refresh(); + + this.context.getBean(SpringSecurityOAuth2MethodSecurityConfiguration.class); + + ClientDetails config = this.context.getBean(ClientDetails.class); + + DelegatingMethodSecurityMetadataSource source = this.context + .getBean(DelegatingMethodSecurityMetadataSource.class); + List sources = source + .getMethodSecurityMetadataSources(); + + assertThat(sources.size(), is(1)); + assertThat(sources.get(0).getClass().getName(), + is(SecuredAnnotationSecurityMetadataSource.class.getName())); + + verifyAuthentication(config, HttpStatus.OK); + } + + @Test + public void testJsr250SecurityAnnotationOverride() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(Jsr250EnabledConfiguration.class, + MinimalSecureWebApplication.class); + this.context.refresh(); + + this.context.getBean(SpringSecurityOAuth2MethodSecurityConfiguration.class); + + ClientDetails config = this.context.getBean(ClientDetails.class); + + DelegatingMethodSecurityMetadataSource source = this.context + .getBean(DelegatingMethodSecurityMetadataSource.class); + List sources = source + .getMethodSecurityMetadataSources(); + + assertThat(sources.size(), is(1)); + assertThat(sources.get(0).getClass().getName(), + is(Jsr250MethodSecurityMetadataSource.class.getName())); + + verifyAuthentication(config, HttpStatus.OK); + } + + @Test + public void testMethodSecurityBackingOff() { + this.context = new AnnotationConfigEmbeddedWebApplicationContext(); + this.context.register(CustomMethodSecurity.class, + TestSecurityConfiguration.class, MinimalSecureWebApplication.class); + this.context.refresh(); + + DelegatingMethodSecurityMetadataSource source = this.context + .getBean(DelegatingMethodSecurityMetadataSource.class); + List sources = source + .getMethodSecurityMetadataSources(); + assertThat(sources.size(), is(1)); + assertThat(sources.get(0).getClass().getName(), + is(PrePostAnnotationSecurityMetadataSource.class.getName())); + } + + /** + * Connect to the oauth service, get a token, and then attempt some operations using + * it. + * + * @param config + */ + private void verifyAuthentication(ClientDetails config) { + verifyAuthentication(config, HttpStatus.FORBIDDEN); + } + + private void verifyAuthentication(ClientDetails config, HttpStatus finalStatus) { + String baseUrl = "http://localhost:" + + this.context.getEmbeddedServletContainer().getPort(); + + RestTemplate rest = new TestRestTemplate(); + HttpHeaders headers = new HttpHeaders(); + + // First, verify the web endpoint can't be reached + ResponseEntity entity = rest.exchange(new RequestEntity(headers, + HttpMethod.GET, URI.create(baseUrl + "/secured")), String.class); + assertThat(entity.getStatusCode(), is(HttpStatus.UNAUTHORIZED)); + + // Since we can't reach it, need to collect an authorization token + String base64Creds = new String( + Base64.encode((config.getClientId() + ":" + config.getClientSecret()) + .getBytes())); + headers.set("Authorization", "Basic " + base64Creds); + + MultiValueMap body = new LinkedMultiValueMap(); + body.set("grant_type", "password"); + body.set("username", "foo"); + body.set("password", "bar"); + body.set("scope", "read"); + + HttpEntity> request = new HttpEntity>( + body, headers); + + JsonNode response = rest.postForObject(baseUrl + "/oauth/token", request, + JsonNode.class); + String authorizationToken = response.findValue("access_token").asText(); + String tokenType = response.findValue("token_type").asText(); + String scope = response.findValues("scope").get(0).toString(); + assertThat(tokenType, is("bearer")); + assertThat(scope, is("\"read\"")); + + // Now we should be able to see that endpoint. + headers.set("Authorization", "BEARER " + authorizationToken); + + ResponseEntity securedResponse = rest.exchange(new RequestEntity( + headers, HttpMethod.GET, URI.create(baseUrl + "/securedFind")), + String.class); + assertThat(securedResponse.getStatusCode(), is(HttpStatus.OK)); + assertThat(securedResponse.getBody(), + is("You reached an endpoint secured by Spring Security OAuth2")); + + entity = rest.exchange( + new RequestEntity(headers, HttpMethod.POST, URI.create(baseUrl + + "/securedSave")), String.class); + assertThat(entity.getStatusCode(), is(finalStatus)); + } + + @Configuration + @Import({ UseFreePortEmbeddedContainerConfiguration.class, + SecurityAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, + SpringSecurityOAuth2AutoConfiguration.class, WebMvcAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class }) + protected static class MinimalSecureWebApplication { + + } + + @Configuration + protected static class TestSecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Override + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser("foo").password("bar").roles("USER"); + } + + @Bean + TestWebApp testWebApp() { + return new TestWebApp(); + } + } + + @Configuration + @EnableAuthorizationServer + @EnableResourceServer + @EnableGlobalMethodSecurity(prePostEnabled = true) + protected static class AuthorizationAndResourceServerConfiguration extends + TestSecurityConfiguration { + } + + @Configuration + @EnableAuthorizationServer + @EnableResourceServer + @EnableGlobalMethodSecurity(securedEnabled = true) + protected static class SecuredEnabledConfiguration extends TestSecurityConfiguration { + } + + @Configuration + @EnableAuthorizationServer + @EnableResourceServer + @EnableGlobalMethodSecurity(jsr250Enabled = true) + protected static class Jsr250EnabledConfiguration extends TestSecurityConfiguration { + } + + @Configuration + @EnableAuthorizationServer + protected static class AuthorizationServerConfiguration extends + TestSecurityConfiguration { + } + + @Configuration + @EnableResourceServer + protected static class ResourceServerConfiguration extends TestSecurityConfiguration { + } + + @RestController + protected static class TestWebApp { + + @RequestMapping(value = "/securedFind", method = RequestMethod.GET) + @PreAuthorize("#oauth2.hasScope('read')") + public String secureFind() { + return "You reached an endpoint secured by Spring Security OAuth2"; + } + + @RequestMapping(value = "/securedSave", method = RequestMethod.POST) + @PreAuthorize("#oauth2.hasScope('write')") + public String secureSave() { + return "You reached an endpoint secured by Spring Security OAuth2"; + } + } + + @Configuration + protected static class UseFreePortEmbeddedContainerConfiguration { + @Bean + TomcatEmbeddedServletContainerFactory containerFactory() { + return new TomcatEmbeddedServletContainerFactory(0); + } + } + + @Configuration + @EnableResourceServer + protected static class CustomResourceServer extends ResourceServerConfigurerAdapter { + + @Autowired + private ResourceServerProperties config; + + @Override + public void configure(ResourceServerSecurityConfigurer resources) + throws Exception { + if (this.config.getId() != null) { + resources.resourceId(this.config.getId()); + } + } + + @Override + public void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated().and().httpBasic().and() + .csrf().disable(); + } + + } + + @Configuration + @EnableAuthorizationServer + protected static class CustomAuthorizationServer extends + AuthorizationServerConfigurerAdapter { + + @Autowired + private AuthenticationManager authenticationManager; + + @Bean + public TokenStore tokenStore() { + return new InMemoryTokenStore(); + } + + @Bean + public ApprovalStore approvalStore(final TokenStore tokenStore) { + TokenApprovalStore approvalStore = new TokenApprovalStore(); + approvalStore.setTokenStore(tokenStore); + return approvalStore; + } + + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + clients.inMemory().withClient("client").secret("secret") + .resourceIds("resource-id").authorizedGrantTypes("password") + .authorities("USER").scopes("read") + .redirectUris("http://localhost:8080"); + } + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) + throws Exception { + endpoints.tokenStore(tokenStore()).authenticationManager( + this.authenticationManager); + } + } + + @Configuration + @EnableGlobalMethodSecurity(prePostEnabled = true) + protected static class CustomMethodSecurity extends GlobalMethodSecurityConfiguration { + @Override + protected MethodSecurityExpressionHandler createExpressionHandler() { + return new OAuth2MethodSecurityExpressionHandler(); + } + } +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java new file mode 100644 index 0000000000..4264c1fc6a --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import static org.junit.Assert.assertNotNull; + +import java.util.Map; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Dave Syer + * + */ +public class ResourceServerPropertiesTests { + + private ResourceServerProperties properties = new ResourceServerProperties("client", "secret"); + + @Test + public void json() throws Exception { + properties.getJwt().setKeyUri("http://example.com/token_key"); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(properties); + @SuppressWarnings("unchecked") + Map value = mapper.readValue(json, Map.class); + @SuppressWarnings("unchecked") + Map jwt = (Map) value.get("jwt"); + assertNotNull("Wrong json: " + json, jwt.get("keyUri")); + } + + @Test + public void tokenKeyDerived() throws Exception { + properties.setUserInfoUri("http://example.com/userinfo"); + assertNotNull("Wrong properties: " + properties, properties.getJwt().getKeyUri()); + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java new file mode 100644 index 0000000000..d953dbf5cb --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import org.junit.After; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.ClientCredentialsProperties; +import org.springframework.boot.autoconfigure.social.FacebookAutoConfiguration; +import org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.security.oauth2.provider.token.DefaultTokenServices; +import org.springframework.security.oauth2.provider.token.RemoteTokenServices; +import org.springframework.social.connect.ConnectionFactoryLocator; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Dave Syer + * + */ +public class ResourceServerTokenServicesConfigurationTests { + + private ConfigurableApplicationContext context; + + private ConfigurableEnvironment environment = new StandardEnvironment(); + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void defaultIsRemoteTokenServices() { + this.context = new SpringApplicationBuilder(ResourceConfiguration.class).web( + false).run(); + RemoteTokenServices services = this.context.getBean(RemoteTokenServices.class); + assertNotNull(services); + } + + @Test + public void useRemoteTokenServices() { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.oauth2.resource.tokenInfoUri:http://example.com", + "spring.oauth2.resource.clientId=acme"); + this.context = new SpringApplicationBuilder(ResourceConfiguration.class) + .environment(this.environment).web(false).run(); + RemoteTokenServices services = this.context.getBean(RemoteTokenServices.class); + assertNotNull(services); + } + + @Test + public void switchToUserInfo() { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.oauth2.resource.userInfoUri:http://example.com"); + this.context = new SpringApplicationBuilder(ResourceConfiguration.class) + .environment(this.environment).web(false).run(); + UserInfoTokenServices services = this.context + .getBean(UserInfoTokenServices.class); + assertNotNull(services); + } + + @Test + public void preferUserInfo() { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.oauth2.resource.userInfoUri:http://example.com", + "spring.oauth2.resource.tokenInfoUri:http://example.com", + "spring.oauth2.resource.preferTokenInfo:false"); + this.context = new SpringApplicationBuilder(ResourceConfiguration.class) + .environment(this.environment).web(false).run(); + UserInfoTokenServices services = this.context + .getBean(UserInfoTokenServices.class); + assertNotNull(services); + } + + @Test + public void switchToJwt() { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.oauth2.resource.jwt.keyValue=FOOBAR"); + this.context = new SpringApplicationBuilder(ResourceConfiguration.class) + .environment(this.environment).web(false).run(); + DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class); + assertNotNull(services); + } + + @Test + public void asymmetricJwt() { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.oauth2.resource.jwt.keyValue=" + publicKey); + this.context = new SpringApplicationBuilder(ResourceConfiguration.class) + .environment(this.environment).web(false).run(); + DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class); + assertNotNull(services); + } + + @Test + public void springSocialUserInfo() { + EnvironmentTestUtils.addEnvironment(this.environment, + "spring.oauth2.resource.userInfoUri:http://example.com", + "spring.social.facebook.app-id=foo", + "spring.social.facebook.app-secret=bar"); + this.context = new SpringApplicationBuilder(SocialResourceConfiguration.class) + .environment(this.environment).web(true).run(); + ConnectionFactoryLocator connectionFactory = this.context + .getBean(ConnectionFactoryLocator.class); + assertNotNull(connectionFactory); + SpringSocialTokenServices services = this.context + .getBean(SpringSocialTokenServices.class); + assertNotNull(services); + } + + @Configuration + @Import({ ResourceServerTokenServicesConfiguration.class, + ResourceServerPropertiesConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + @EnableConfigurationProperties(ClientCredentialsProperties.class) + protected static class ResourceConfiguration { + } + + @Configuration + protected static class ResourceServerPropertiesConfiguration { + + @Autowired + private ClientCredentialsProperties credentials; + + @Bean + public ResourceServerProperties resourceServerProperties() { + return new ResourceServerProperties(this.credentials.getClientId(), + this.credentials.getClientSecret()); + } + } + + @Import({ FacebookAutoConfiguration.class, SocialWebAutoConfiguration.class }) + protected static class SocialResourceConfiguration extends ResourceConfiguration { + @Bean + public EmbeddedServletContainerFactory embeddedServletContainerFactory() { + return Mockito.mock(EmbeddedServletContainerFactory.class); + } + } + + private static String publicKey = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGp/Q5lh0P8nPL21oMMrt2RrkT9AW5jgYwLfSUnJVc9G6uR3cXRRDCjHqWU5WYwivcF180A6CWp/ireQFFBNowgc5XaA0kPpzEtgsA5YsNX7iSnUibB004iBTfU9hZ2Rbsc8cWqynT0RyN4TP1RYVSeVKvMQk4GT1r7JCEC+TNu1ELmbNwMQyzKjsfBXyIOCFU/E94ktvsTZUHF4Oq44DBylCDsS1k7/sfZC2G5EU7Oz0mhG8+Uz6MSEQHtoIi6mc8u64Rwi3Z3tscuWG2ShtsUFuNSAFNkY7LkLn+/hxLCu2bNISMaESa8dG22CIMuIeRLVcAmEWEWH5EEforTg+QIDAQAB\n" + + "-----END PUBLIC KEY-----"; + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesTests.java new file mode 100644 index 0000000000..5296bfde10 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServicesTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; + +/** + * @author Dave Syer + * + */ +public class UserInfoTokenServicesTests { + + private UserInfoTokenServices services = new UserInfoTokenServices( + "http://example.com", "foo"); + private BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); + private OAuth2RestOperations template = Mockito.mock(OAuth2RestOperations.class); + private Map map = new LinkedHashMap(); + + @Before + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void init() { + resource.setClientId("foo"); + Mockito.when( + template.getForEntity(Mockito.any(String.class), Mockito.any(Class.class))) + .thenReturn(new ResponseEntity(map, HttpStatus.OK)); + Mockito.when(template.getAccessToken()).thenReturn(new DefaultOAuth2AccessToken("FOO")); + Mockito.when(template.getResource()).thenReturn(resource); + Mockito.when(template.getOAuth2ClientContext()).thenReturn( + Mockito.mock(OAuth2ClientContext.class)); + } + + @Test + public void sunnyDay() { + services.setResources(Collections.singletonMap("foo", template)); + assertEquals("unknown", services.loadAuthentication("FOO").getName()); + } + + @Test + public void userId() { + map.put("userid", "spencer"); + services.setResources(Collections.singletonMap("foo", template)); + assertEquals("spencer", services.loadAuthentication("FOO").getName()); + } + +} diff --git a/spring-boot-cli/samples/oauth2.groovy b/spring-boot-cli/samples/oauth2.groovy new file mode 100644 index 0000000000..8ecd32b4e8 --- /dev/null +++ b/spring-boot-cli/samples/oauth2.groovy @@ -0,0 +1,15 @@ +package org.test + +@EnableAuthorizationServer +@EnableResourceServer +@EnableGlobalMethodSecurity(prePostEnabled = true) +@RestController +class SampleController { + + @PreAuthorize("#oauth2.hasScope('read')") + @RequestMapping("/") + def hello() { + [message: "Hello World!"] + } + +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringSecurityOAuth2CompilerAutoConfiguration.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringSecurityOAuth2CompilerAutoConfiguration.java new file mode 100644 index 0000000000..58b9c76fda --- /dev/null +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/autoconfigure/SpringSecurityOAuth2CompilerAutoConfiguration.java @@ -0,0 +1,58 @@ +/* +<<<<<<< HEAD + * Copyright 2012-2014 the original author or authors. +======= + * Copyright 2012-2013 the original author or authors. +>>>>>>> 12b17e3... Add Spring Security OAuth2 support to Spring Boot CLI + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.cli.compiler.autoconfigure; + +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.springframework.boot.cli.compiler.AstUtils; +import org.springframework.boot.cli.compiler.CompilerAutoConfiguration; +import org.springframework.boot.cli.compiler.DependencyCustomizer; + +/** + * {@link CompilerAutoConfiguration} for Spring Security OAuth2. + * + * @author Greg Turnquist + */ +public class SpringSecurityOAuth2CompilerAutoConfiguration extends CompilerAutoConfiguration { + + @Override + public boolean matches(ClassNode classNode) { + return AstUtils.hasAtLeastOneAnnotation(classNode, + "EnableAuthorizationServer", "EnableResourceServer"); + } + + @Override + public void applyDependencies(DependencyCustomizer dependencies) throws CompilationFailedException { + dependencies.add("spring-security-oauth2").add("spring-boot-starter-web") + .add("spring-boot-starter-security"); + } + + @Override + public void applyImports(ImportCustomizer imports) throws CompilationFailedException { + imports + .addImports( + "org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity") + .addStarImports( + "org.springframework.security.oauth2.config.annotation.web.configuration", + "org.springframework.security.access.prepost"); + } +} diff --git a/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration b/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration index c3340efaeb..05e8637a54 100644 --- a/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration +++ b/spring-boot-cli/src/main/resources/META-INF/services/org.springframework.boot.cli.compiler.CompilerAutoConfiguration @@ -11,7 +11,9 @@ org.springframework.boot.cli.compiler.autoconfigure.JUnitCompilerAutoConfigurati org.springframework.boot.cli.compiler.autoconfigure.SpockCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.TransactionManagementCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.SpringIntegrationCompilerAutoConfiguration +org.springframework.boot.cli.compiler.autoconfigure.SpringSecurityOAuth2CompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.SpringSecurityCompilerAutoConfiguration +org.springframework.boot.cli.compiler.autoconfigure.SpringSecurityOAuth2CompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.SpringMobileCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.SpringSocialFacebookCompilerAutoConfiguration org.springframework.boot.cli.compiler.autoconfigure.SpringSocialLinkedInCompilerAutoConfiguration diff --git a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java index d0453f119b..9250a30e7c 100644 --- a/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java +++ b/spring-boot-cli/src/test/java/org/springframework/boot/cli/SampleIntegrationTests.java @@ -69,6 +69,18 @@ public class SampleIntegrationTests { output.contains("completed with the following parameters")); } + @Test + @Ignore("Spring Security Oauth2 autoconfiguration reports bean creation issue with methodSecurityInterceptor") + public void oauth2Sample() throws Exception { + String output = this.cli.run("oauth2.groovy"); + assertTrue("Wrong output: " + output, output.contains("spring.oauth2.clientId")); + assertTrue("Wrong output: " + output, output.contains("spring.oauth2.secret = ****")); + assertTrue("Wrong output: " + output, output.contains("spring.oauth2.resourceId")); + assertTrue("Wrong output: " + output, output.contains("spring.oauth2.authorizationTypes")); + assertTrue("Wrong output: " + output, output.contains("spring.oauth2.scopes")); + assertTrue("Wrong output: " + output, output.contains("spring.oauth2.redirectUris")); + } + @Test public void reactorSample() throws Exception { String output = this.cli.run("reactor.groovy", "Phil"); diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 44bf366f99..48599314a6 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -135,8 +135,12 @@ 2.0.1.RELEASE 1.0.1.RELEASE 1.1.0.RELEASE + 3.2.5.RELEASE + 2.0.5.RELEASE + 1.0.2.RELEASE 2.2.1.RELEASE 3.1.0 + 2.1.0 ${javax-mail.version} 2.1.4.RELEASE 2.1.2.RELEASE @@ -1521,6 +1525,11 @@ spring-security-jwt ${spring-security-jwt.version} + + org.springframework.security.oauth + spring-security-oauth2 + ${spring-security-oauth2.version} + org.springframework.social spring-social-config @@ -1864,4 +1873,4 @@ integration-test - \ No newline at end of file + diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 277050ddfb..c81ca5b79c 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -58,6 +58,7 @@ spring-boot-sample-parent-context spring-boot-sample-profile spring-boot-sample-secure + spring-boot-sample-secure-oauth2 spring-boot-sample-servlet spring-boot-sample-simple spring-boot-sample-testng diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/pom.xml b/spring-boot-samples/spring-boot-sample-secure-oauth2/pom.xml new file mode 100644 index 0000000000..9aaf7f76d7 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + spring-boot-sample-secure-oauth2 + Spring Boot Security OAuth2 Sample + Spring Boot Security OAuth2 Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-rest + + + com.h2database + h2 + + + org.springframework.security.oauth + spring-security-oauth2 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Application.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Application.java new file mode 100644 index 0000000000..1a5fd5f639 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Application.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; + +// @formatter:off +/** + * After you launch the app, you can seek a bearer token like this: + * + *
+ *
+ * curl localhost:8080/oauth/token -d "grant_type=password&scope=read&username=greg&password=turnquist" -u foo:bar
+ *
+ * 
+ * + * + * + * Response should be similar to this: + * {"access_token":"533de99b-5a0f-4175-8afd-1a64feb952d5","token_type":"bearer","expires_in":43199,"scope":"read"} + * + * With the token value, you can now interrogate the RESTful interface like this: + * + *
+ * curl -H "Authorization: bearer [access_token]" localhost:8080/flights/1
+ * 
+ * + * You should then see the pre-loaded data like this: + * + *
+ * {
+ *      "origin" : "Nashville",
+ *      "destination" : "Dallas",
+ *      "airline" : "Spring Ways",
+ *      "flightNumber" : "OAUTH2",
+ *      "date" : null,
+ *      "traveler" : "Greg Turnquist",
+ *      "_links" : {
+ *          "self" : {
+ *              "href" : "http://localhost:8080/flights/1"
+ *          }
+ *      }
+ * }
+ * 
+ * + * Test creating a new entry: + * + *
+ * curl -i -H "Authorization: bearer [access token]" -H "Content-Type:application/json" localhost:8080/flights -X POST -d @flight.json
+ * 
+ * + * Insufficient scope? (read not write) Ask for a new token! + * + *
+ * curl localhost:8080/oauth/token -d "grant_type=password&scope=write&username=greg&password=turnquist" -u foo:bar
+ *
+ * {"access_token":"cfa69736-e2aa-4ae7-abbb-3085acda560e","token_type":"bearer","expires_in":43200,"scope":"write"}
+ * 
+ * + * Retry with the new token. There should be a Location header. + * + *
+ * Location: http://localhost:8080/flights/2
+ *
+ * curl -H "Authorization: bearer [access token]" localhost:8080/flights/2
+ * 
+ * + * @author Craig Walls + * @author Greg Turnquist + */ +// @formatter:on + +@Configuration +@ComponentScan +@EnableAutoConfiguration +@EnableAuthorizationServer +@EnableResourceServer +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Flight.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Flight.java new file mode 100644 index 0000000000..0319074702 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Flight.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Domain object for tracking flights + * + * @author Craig Walls + * @author Greg Turnquist + */ +@Entity +@JsonIgnoreProperties(ignoreUnknown = true) +public class Flight { + + @Id @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String origin; + private String destination; + private String airline; + private String flightNumber; + private Date date; + private String traveler; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getOrigin() { + return origin; + } + + public void setOrigin(String origin) { + this.origin = origin; + } + + public String getDestination() { + return destination; + } + + public void setDestination(String destination) { + this.destination = destination; + } + + public String getAirline() { + return airline; + } + + public void setAirline(String airline) { + this.airline = airline; + } + + public String getFlightNumber() { + return flightNumber; + } + + public void setFlightNumber(String flightNumber) { + this.flightNumber = flightNumber; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public String getTraveler() { + return traveler; + } + + public void setTraveler(String traveler) { + this.traveler = traveler; + } +} diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/FlightRepository.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/FlightRepository.java new file mode 100644 index 0000000000..811b062af5 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/FlightRepository.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.security.access.prepost.PreAuthorize; + +/** + * Spring Data interface with secured methods + * + * @author Craig Walls + * @author Greg Turnquist + */ +public interface FlightRepository extends CrudRepository { + + @PreAuthorize("#oauth2.hasScope('read')") + @Override + Iterable findAll(); + + @PreAuthorize("#oauth2.hasScope('read')") + @Override + Flight findOne(Long aLong); + + @PreAuthorize("#oauth2.hasScope('write')") + @Override + S save(S entity); +} diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties new file mode 100644 index 0000000000..32262012db --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.datasource.platform=h2 +spring.oauth2.client.client-id=foo +spring.oauth2.client.client-secret=bar + +security.user.name=greg +security.user.password=turnquist + +logging.level.org.springframework.security=DEBUG diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/data-h2.sql b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/data-h2.sql new file mode 100644 index 0000000000..478a2ebf91 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/data-h2.sql @@ -0,0 +1,4 @@ +insert into FLIGHT +(id, origin, destination, airline, flight_number, traveler) +values +(1, 'Nashville', 'Dallas', 'Spring Ways', 'OAUTH2', 'Greg Turnquist'); diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/templates/.gitignore b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/templates/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/test/java/sample/ApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/test/java/sample/ApplicationTests.java new file mode 100644 index 0000000000..abe9dc7064 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/test/java/sample/ApplicationTests.java @@ -0,0 +1,145 @@ +package sample; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.hateoas.MediaTypes; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +/** + * Series of automated integration tests to verify proper behavior of auto-configured, + * OAuth2-secured system + * + * @author Greg Turnquist + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@SpringApplicationConfiguration(classes = Application.class) +@IntegrationTest("server.port:0") +public class ApplicationTests { + + @Autowired + WebApplicationContext context; + @Autowired + FilterChainProxy filterChain; + + private MockMvc mvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Before + public void setUp() { + + this.mvc = webAppContextSetup(this.context).addFilters(this.filterChain).build(); + SecurityContextHolder.clearContext(); + } + + @Test + public void everythingIsSecuredByDefault() throws Exception { + + this.mvc.perform(get("/").// + accept(MediaTypes.HAL_JSON)).// / + andExpect(status().isUnauthorized()).// + andDo(print()); + + this.mvc.perform(get("/flights").// + accept(MediaTypes.HAL_JSON)).// / + andExpect(status().isUnauthorized()).// + andDo(print()); + + this.mvc.perform(get("/flights/1").// + accept(MediaTypes.HAL_JSON)).// / + andExpect(status().isUnauthorized()).// + andDo(print()); + + this.mvc.perform(get("/alps").// + accept(MediaTypes.HAL_JSON)).// / + andExpect(status().isUnauthorized()).// + andDo(print()); + } + + @Test + @Ignore + // TODO: maybe show mixed basic + token auth on different resources? + public void accessingRootUriPossibleWithUserAccount() throws Exception { + + this.mvc.perform( + get("/").// + accept(MediaTypes.HAL_JSON).// + header("Authorization", + "Basic " + + new String(Base64.encode("greg:turnquist" + .getBytes())))) + .// + andExpect(header().string("Content-Type", MediaTypes.HAL_JSON.toString())) + .// + andExpect(status().isOk()).// + andDo(print()); + } + + @Test + public void useAppSecretsPlusUserAccountToGetBearerToken() throws Exception { + + MvcResult result = this.mvc + .perform( + get("/oauth/token").// + header("Authorization", + "Basic " + + new String(Base64.encode("foo:bar" + .getBytes()))).// + param("grant_type", "password").// + param("scope", "read").// + param("username", "greg").// + param("password", "turnquist")).// + andExpect(status().isOk()).// + andDo(print()).// + andReturn(); + + Object accessToken = this.objectMapper.readValue( + result.getResponse().getContentAsString(), Map.class).get("access_token"); + + MvcResult flightsAction = this.mvc + .perform(get("/flights/1").// + accept(MediaTypes.HAL_JSON).// + header("Authorization", "Bearer " + accessToken)) + .// + andExpect(header().string("Content-Type", MediaTypes.HAL_JSON.toString())) + .// + andExpect(status().isOk()).// + andDo(print()).// + andReturn(); + + Flight flight = this.objectMapper.readValue(flightsAction.getResponse() + .getContentAsString(), Flight.class); + + assertThat(flight.getOrigin(), is("Nashville")); + assertThat(flight.getDestination(), is("Dallas")); + assertThat(flight.getAirline(), is("Spring Ways")); + assertThat(flight.getFlightNumber(), is("OAUTH2")); + assertThat(flight.getTraveler(), is("Greg Turnquist")); + } + +}