From af320b49bfc26f994fdf2a004aa663198c100ecc Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 22 May 2015 16:32:47 +0100 Subject: [PATCH] Rationalize some features and merge in customizers from Spring Cloud --- ...ringSecurityOAuth2ClientConfiguration.java | 163 +++++++++++------- .../JwtAccessTokenConverterConfigurer.java | 24 +++ .../resource/ResourceServerProperties.java | 14 ++ ...ourceServerTokenServicesConfiguration.java | 158 +++++++++++++---- .../UserInfoRestTemplateCustomizer.java | 41 +++++ .../resource/UserInfoTokenServices.java | 43 ++--- .../main/resources/META-INF/spring.factories | 2 +- .../AutoConfigureConfigurationClassTests.java | 2 +- ...gSecurityOAuth2AutoConfigurationTests.java | 10 +- .../resource/UserInfoTokenServicesTests.java | 5 +- spring-boot-dependencies/pom.xml | 2 +- .../main/asciidoc/spring-boot-features.adoc | 156 +++++++++++++++++ .../src/main/java/sample/Application.java | 8 +- .../test/java/sample/ApplicationTests.java | 37 ++-- 14 files changed, 503 insertions(+), 162 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtAccessTokenConverterConfigurer.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoRestTemplateCustomizer.java 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 index 88000cccb0e..0fdc938e39c 100644 --- 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 @@ -15,9 +15,6 @@ */ package org.springframework.boot.autoconfigure.security.oauth2.client; -import java.io.IOException; -import java.util.Arrays; - import javax.annotation.PostConstruct; import javax.annotation.Resource; @@ -27,6 +24,10 @@ 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.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.oauth2.ClientCredentialsProperties; import org.springframework.boot.context.embedded.FilterRegistrationBean; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -35,25 +36,22 @@ 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.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; 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.DefaultAccessTokenRequest; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; 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; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; /** * @author Dave Syer @@ -61,84 +59,119 @@ import org.springframework.util.MultiValueMap; */ @Configuration @ConditionalOnClass(EnableOAuth2Client.class) -@ConditionalOnBean(OAuth2ClientConfiguration.class) +@ConditionalOnExpression("'${spring.oauth2.client.clientId:}'!=''") public class SpringSecurityOAuth2ClientConfiguration { private static final Log logger = LogFactory .getLog(SpringSecurityOAuth2ClientConfiguration.class); + @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 + @Primary + public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, + OAuth2ProtectedResourceDetails details) { + OAuth2RestTemplate template = new OAuth2RestTemplate(details, oauth2ClientContext); + return template; + } + @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() : "****")); - } + protected abstract static class BaseConfiguration { @Bean @ConfigurationProperties("spring.oauth2.client") @Primary - public AuthorizationCodeResourceDetails authorizationCodeResourceDetails() { + public AuthorizationCodeResourceDetails oauth2RemoteResource() { AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); - details.setClientSecret(this.credentials.getClientSecret()); - details.setClientId(this.credentials.getClientId()); return details; } + } + + @Configuration + @ConditionalOnNotWebApplication + protected static class SingletonScopedConfiguration { + + @Bean + @ConfigurationProperties("spring.oauth2.client") + @Primary + public ClientCredentialsResourceDetails oauth2RemoteResource() { + ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails(); + return details; + } + + @Bean + public OAuth2ClientContext oauth2ClientContext() { + return new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()); + } + + } + + @Configuration + @ConditionalOnBean(OAuth2ClientConfiguration.class) + @ConditionalOnWebApplication + protected static class SessionScopedConfiguration extends BaseConfiguration { + + @Resource + @Qualifier("accessTokenRequest") + protected AccessTokenRequest accessTokenRequest; + + @Bean + @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES) + public OAuth2ClientContext oauth2ClientContext() { + return new DefaultOAuth2ClientContext(accessTokenRequest); + } + @Bean public FilterRegistrationBean oauth2ClientFilterRegistration( OAuth2ClientContextFilter filter) { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(filter); - registration.setOrder(0); + registration.setOrder(-100); 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; - } + } + + /* + * When the authentication is per cookie but the stored token is an oauth2 one, we can + * pass that on to a client that wants to call downstream. We don't even need an + * OAuth2ClientContextFilter until we need to refresh the access token. To handle + * refresh tokens you need to @EnableOAuth2Client + */ + @Configuration + @ConditionalOnMissingBean(OAuth2ClientConfiguration.class) + @ConditionalOnWebApplication + protected static class RequestScopedConfiguration extends BaseConfiguration { @Bean @Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES) public OAuth2ClientContext oauth2ClientContext() { - return new DefaultOAuth2ClientContext(this.accessTokenRequest); + DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext( + new DefaultAccessTokenRequest()); + Authentication principal = SecurityContextHolder.getContext() + .getAuthentication(); + if (principal instanceof OAuth2Authentication) { + OAuth2Authentication authentication = (OAuth2Authentication) principal; + Object details = authentication.getDetails(); + if (details instanceof OAuth2AuthenticationDetails) { + OAuth2AuthenticationDetails oauthsDetails = (OAuth2AuthenticationDetails) details; + String token = oauthsDetails.getTokenValue(); + context.setAccessToken(new DefaultOAuth2AccessToken(token)); + } + } + return context; } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtAccessTokenConverterConfigurer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtAccessTokenConverterConfigurer.java new file mode 100644 index 00000000000..2ebf2d7ce6d --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtAccessTokenConverterConfigurer.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; + +public interface JwtAccessTokenConverterConfigurer { + + void configure(JwtAccessTokenConverter converter); + +} 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 index 149244f819a..5e26e7ae7a5 100644 --- 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 @@ -21,6 +21,7 @@ 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.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; import org.springframework.util.StringUtils; import org.springframework.validation.Errors; @@ -66,6 +67,11 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware { */ private boolean preferTokenInfo = true; + /** + * The token type to send when using the userInfoUri. + */ + private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE; + private Jwt jwt = new Jwt(); public ResourceServerProperties() { @@ -126,6 +132,14 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware { this.preferTokenInfo = preferTokenInfo; } + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + public Jwt getJwt() { return this.jwt; } 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 index f062635382c..14c99217b55 100644 --- 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 @@ -15,32 +15,48 @@ */ package org.springframework.boot.autoconfigure.security.oauth2.resource; +import java.io.IOException; +import java.util.Arrays; import java.util.Collections; +import java.util.List; 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.beans.factory.annotation.Qualifier; 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.OrderComparator; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestOperations; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +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.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; @@ -49,6 +65,7 @@ import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenCo 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.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; @@ -64,29 +81,88 @@ public class ResourceServerTokenServicesConfiguration { private static final Log logger = LogFactory .getLog(ResourceServerTokenServicesConfiguration.class); + @Configuration + protected static class UserInfoRestTemplateConfiguration { + + private static final AuthorizationCodeResourceDetails DEFAULT_RESOURCE_DETAILS = new AuthorizationCodeResourceDetails(); + + static { + DEFAULT_RESOURCE_DETAILS.setClientId(""); + DEFAULT_RESOURCE_DETAILS + .setUserAuthorizationUri("Not a URI because there is no client"); + DEFAULT_RESOURCE_DETAILS + .setAccessTokenUri("Not a URI because there is no client"); + } + + @Autowired(required = false) + private List customizers = Collections + .emptyList(); + + @Autowired(required = false) + private OAuth2ProtectedResourceDetails details; + + @Autowired(required = false) + private OAuth2ClientContext oauth2ClientContext; + + @Bean(name = "userInfoRestTemplate") + public OAuth2RestTemplate userInfoRestTemplate() { + OAuth2RestTemplate template; + if (details == null) { + details = DEFAULT_RESOURCE_DETAILS; + } + if (oauth2ClientContext == null) { + template = new OAuth2RestTemplate(details); + } + else { + template = new OAuth2RestTemplate(details, 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); + OrderComparator.sort(customizers); + for (UserInfoRestTemplateCustomizer customizer : customizers) { + customizer.customize(template); + } + return template; + } + + } + @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()); + services.setClientId(this.resource.getClientId()); + services.setClientSecret(this.resource.getClientSecret()); return services; } @@ -100,21 +176,19 @@ public class ResourceServerTokenServicesConfiguration { @Autowired private ResourceServerProperties sso; - @Autowired - private ClientCredentialsProperties client; - @Autowired(required = false) private OAuth2ConnectionFactory connectionFactory; @Autowired(required = false) - private Map resources = Collections.emptyMap(); + @Qualifier("userInfoRestTemplate") + private OAuth2RestOperations restTemplate; @Bean @ConditionalOnBean(ConnectionFactoryLocator.class) @ConditionalOnMissingBean(ResourceServerTokenServices.class) public SpringSocialTokenServices socialTokenServices() { return new SpringSocialTokenServices(this.connectionFactory, - this.client.getClientId()); + this.sso.getClientId()); } @Bean @@ -122,33 +196,33 @@ public class ResourceServerTokenServicesConfiguration { ResourceServerTokenServices.class }) public ResourceServerTokenServices userInfoTokenServices() { UserInfoTokenServices services = new UserInfoTokenServices( - this.sso.getUserInfoUri(), this.client.getClientId()); - services.setResources(this.resources); + this.sso.getUserInfoUri(), this.sso.getClientId()); + services.setTokenType(sso.getTokenType()); + services.setRestTemplate(restTemplate); return services; } } @Configuration - @ConditionalOnMissingClass(name = "org.springframework.social.connect.support.OAuth2ConnectionFactory") + @ConditionalOnMissingClass("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(); + @Qualifier("userInfoRestTemplate") + private OAuth2RestOperations restTemplate; @Bean @ConditionalOnMissingBean(ResourceServerTokenServices.class) public ResourceServerTokenServices userInfoTokenServices() { UserInfoTokenServices services = new UserInfoTokenServices( - this.sso.getUserInfoUri(), this.client.getClientId()); - services.setResources(this.resources); + this.sso.getUserInfoUri(), this.sso.getClientId()); + services.setRestTemplate(restTemplate); + services.setTokenType(sso.getTokenType()); return services; } @@ -160,9 +234,15 @@ public class ResourceServerTokenServicesConfiguration { @Conditional(JwtToken.class) protected static class JwtTokenServicesConfiguration { + private RestTemplate keyUriRestTemplate = new RestTemplate(); + @Autowired private ResourceServerProperties resource; + @Autowired(required = false) + private List configurers = Collections + .emptyList(); + @Bean @ConditionalOnMissingBean(ResourceServerTokenServices.class) public ResourceServerTokenServices jwtTokenServices() { @@ -182,22 +262,34 @@ public class ResourceServerTokenServicesConfiguration { String keyValue = this.resource.getJwt().getKeyValue(); if (!StringUtils.hasText(keyValue)) { try { - keyValue = (String) new RestTemplate().getForObject( - this.resource.getJwt().getKeyUri(), Map.class).get("value"); + HttpHeaders headers = new HttpHeaders(); + if (resource.getClientId() != null + && resource.getClientSecret() != null) { + byte[] token = Base64 + .encode((resource.getClientId() + ":" + resource + .getClientSecret()).getBytes()); + headers.add("Authorization", "Basic " + new String(token)); + } + HttpEntity requestEntity = new HttpEntity(headers); + keyValue = (String) keyUriRestTemplate + .exchange(resource.getJwt().getKeyUri(), HttpMethod.GET, + requestEntity, Map.class).getBody().get("value"); } catch (ResourceAccessException e) { // ignore 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 (StringUtils.hasText(keyValue) && !keyValue.startsWith("-----BEGIN")) { + converter.setSigningKey(keyValue); } if (keyValue != null) { converter.setVerifierKey(keyValue); } + AnnotationAwareOrderComparator.sort(configurers); + for (JwtAccessTokenConverterConfigurer configurer : configurers) { + configurer.configure(converter); + } return converter; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoRestTemplateCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoRestTemplateCustomizer.java new file mode 100644 index 00000000000..fdc1f11ec0e --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoRestTemplateCustomizer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import org.springframework.security.oauth2.client.OAuth2RestTemplate; + +/** + * Callback for customizing the rest template used to fetch user details if authentication + * is done via OAuth2 access tokens. The default should be fine for most providers, but + * occasionally you might need to add additional interceptors, or change the request + * authenticator (which is how the token gets attached to outgoing requests). The rest + * template that is being customized here is only used internally to carry out + * authentication (in the SSO or Resource Server use cases). + * + * @author Dave Syer + * + */ +public interface UserInfoRestTemplateCustomizer { + + /** + * Customize the rest template before it is initialized. + * + * @param template the rest template + */ + void customize(OAuth2RestTemplate template); + +} 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 index c482eee0a08..6909998bf9c 100644 --- 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 @@ -15,11 +15,7 @@ */ 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; @@ -44,22 +40,21 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { private String clientId; - private Collection resources = Collections.emptySet(); + private OAuth2RestOperations restTemplate; + + private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE; public UserInfoTokenServices(String userInfoEndpointUrl, String clientId) { this.userInfoEndpointUrl = userInfoEndpointUrl; this.clientId = clientId; } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } - 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); - } - } + public void setRestTemplate(OAuth2RestOperations restTemplate) { + this.restTemplate = restTemplate; } @Override @@ -87,8 +82,8 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { } private Object getPrincipal(Map map) { - String[] keys = new String[] { "user", "username", "userid", "user_id", "login", - "id" }; + String[] keys = new String[] { "user", "username", "userid", "user_id", "login", + "id", "name" }; for (String key : keys) { if (map.containsKey(key)) { return map.get(key); @@ -104,23 +99,15 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { 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) { - } - } + OAuth2RestOperations restTemplate = this.restTemplate; if (restTemplate == null) { BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); resource.setClientId(clientId); restTemplate = new OAuth2RestTemplate(resource); - restTemplate.getOAuth2ClientContext().setAccessToken( - new DefaultOAuth2AccessToken(accessToken)); } + DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken); + token.setTokenType(tokenType); + restTemplate.getOAuth2ClientContext().setAccessToken(token); @SuppressWarnings("rawtypes") Map map = restTemplate.getForEntity(path, Map.class).getBody(); @SuppressWarnings("unchecked") 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 2f9a1d8018e..07d444ffce5 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -51,8 +51,8 @@ org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration,\ 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.sendgrid.SendGridAutoConfiguration,\ 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/AutoConfigureConfigurationClassTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigureConfigurationClassTests.java index ffb50bdb088..1c042940a51 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigureConfigurationClassTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigureConfigurationClassTests.java @@ -19,7 +19,7 @@ package org.springframework.boot.autoconfigure; import org.springframework.boot.test.AbstractConfigurationClassTests; /** - * Tests for the autoconfigure module's @Configuration classes + * Tests for the autoconfigure module's @Configuration classes * @author Andy Wilkinson */ public class AutoConfigureConfigurationClassTests extends AbstractConfigurationClassTests { 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 index dc0a8ab1fa9..f6d914a0af0 100644 --- 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 @@ -16,12 +16,15 @@ package org.springframework.boot.autoconfigure.security.oauth2; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + 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; @@ -74,7 +77,6 @@ import org.springframework.security.oauth2.provider.approval.ApprovalStoreUserAp 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; @@ -90,10 +92,6 @@ 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 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 index 5296bfde10a..e218e7c4a11 100644 --- 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 @@ -17,7 +17,6 @@ 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; @@ -58,14 +57,14 @@ public class UserInfoTokenServicesTests { @Test public void sunnyDay() { - services.setResources(Collections.singletonMap("foo", template)); + services.setRestTemplate(template); assertEquals("unknown", services.loadAuthentication("FOO").getName()); } @Test public void userId() { map.put("userid", "spencer"); - services.setResources(Collections.singletonMap("foo", template)); + services.setRestTemplate(template); assertEquals("spencer", services.loadAuthentication("FOO").getName()); } diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 48599314a6a..00eab3e6014 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -136,7 +136,7 @@ 1.0.1.RELEASE 1.1.0.RELEASE 3.2.5.RELEASE - 2.0.5.RELEASE + 2.0.7.RELEASE 1.0.2.RELEASE 2.2.1.RELEASE 3.1.0 diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index f3d6a2ccbfd..60f454482d9 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1460,6 +1460,162 @@ All of the above can be switched on and off or modified using external propertie features add a `@Bean` of type `WebSecurityConfigurerAdapter` with `@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)`. +=== OAuth2 + +If you have `spring-security-oauth2` on your classpath you can take advantage of some +autoconfiguration to make it easy to set up Authorization or Resource Server features by +configuring some property values in the `Environment`. + +==== Authorization Server + +To create an Authorization Server and grant access tokens you need to +`@EnableAuthorizationServer` and provide +`spring.oauth2.client.[clientId,clientSecret]`. The client will be +registered for you in an in-memory repository. To switch off the +autoconfiguration and configure the Authorization Server features +yourself just add a `@Bean` of type +`AuthorizationServerConfigurer`. Having done that you will be able to +usethe client credentials to create an access token, e.g. + +---- +$ curl client:secret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=pwd +---- + +The basic auth credentials for the `/token` endpoint are the client id +and secret, and the user credentials are the normal Spring Security +user details (which default in Spring Boot to "user" and a random +password). + +==== Resource Server + +To use the access token you need a Resource Server (which can be the +same as the Authorization Server). Creating a Resource Server is easy: +just add `@EnableResourceServer` and provide some configuration to +allow the server to decode access tokens. If your app is also an +Authorization Server it already knows how to decode tokens, so there +is nothing else to do. If your app is a standalone service then you +need to give it some more configuration. Here are the options, one of +the following: + +* `spring.oauth2.resource.userInfoUri` to use the "/me" resource +(e.g. "https://uaa.run.pivotal.io/userinfo" on PWS), or + +* `spring.oauth2.resource.tokenInfoUri` to use the token decoding endpoint +(e.g. "https://uaa.run.pivotal.io/check_token" on PWS). + +If you specify both the `userInfoUri` and the `tokenInfoUri` then +you can set a flag to say that one is preferred over the other +(`preferTokenInfo=true` is the default). + +Alternatively (instead of `userInfoUri` or `tokenInfoUri`) if the +tokens are JWTs you can configure a +`spring.oauth2.resource.jwt.keyValue` to decode them locally, +where the key is a verification key. The verification key value is +either a symmetric secret or PEM-encoded RSA public key. If you don't +have the key and it's public you can provide a URI where it can be +downloaded (as a JSON object with a "value" field) with +`spring.oauth2.resource.jwt.keyUri`. E.g. on PWS: + +---- +$ curl https://uaa.run.pivotal.io/token_key +{"alg":"SHA256withRSA","value":"-----BEGIN PUBLIC KEY-----\nMIIBI...\n-----END PUBLIC KEY-----\n"} +---- + +WARNING: If you use the `spring.oauth2.resource.jwt.keyUri` the +authorization server needs to be running when your application starts +up. It will log a warning if it can't find the key, and tell you what +to do to fix it. + +=== Token Type in User Info + +Google (and certain other 3rd party identity providers) is more strict +about the token type name that is sent in the headers to the user info +endpoint. The default is "Bearer" which suits most providers and +matches the spec, but if you need to change it you can set +`spring.oauth2.resource.tokenType`. + +=== Customizing the User Info RestTemplate + +If you have a `userInfoUri`, the Resource Server features use an +`OAuth2RestTemplate` internally to fetch user details for +authentication. This is provided as a qualified `@Bean` with id +"userInfoRestTemplate", but you shouldn't need to know that to just +use it. The default should be fine for most providers, but +occasionally you might need to add additional interceptors, or change +the request authenticator (which is how the token gets attached to +outgoing requests). To add a customization just create a bean of type +`UserInfoRestTemplateCustomizer` - it has a single method that will be +called after the bean is created but before it is initialized. The +rest template that is being customized here is _only_ used internally +to carry out authentication. + +[TIP] +==== +To set an RSA key value in YAML use the "pipe" continuation +marker to split it over multiple lines ("|") and remember to indent +the key value (it's a standard YAML language feature). Example: + +[source,yaml,indent=0] +---- + oauth2: + resource: + jwt: + keyValue: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC... + -----END PUBLIC KEY----- +---- +==== + +==== Client + +To make your webapp into an OAuth2 client you can simply +`@EnableOAuth2Client` and Spring Boot will create an +`OAuth2RestTemplate` for you to autowire. It uses the +`spring.oauth2.client.*` as credentials (the same as you might be +using in the Authorization Server), but in addition it will need to +know the authorization and token URIs in the Authorization Server. For +example: + +.application.yml +[source,yaml] +---- +spring: + oauth2: + client: + clientId: bd1c0a783ccdd1c9b9e4 + clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1 + accessTokenUri: https://github.com/login/oauth/access_token + userAuthorizationUri: https://github.com/login/oauth/authorize + clientAuthenticationScheme: form + resource: + userInfoUri: https://api.github.com/user + preferTokenInfo: false +---- + +An app with this configuration will redirect to github for +authorization if you attempt to use the `OAuth2RestTemplate`. If you +are already signed into github you won't even notice that it has +authenticated. These specific credentials will only work if your app +is running on port 8080 (register your own client app in Github or +other provider for more flexibility). + +To limit the scope that the client asks for when it obtains an access token +you can set `spring.oauth2.client.scope` (comma separated or an array in YAML). By +default the scope is empty and it is up to to Authorization Server to +decide what the defaults should be, usually depending on the settings in +the client registration that it holds. + +NOTE: There is also a setting for +`spring.oauth2.client.clientAuthenticationScheme` which defaults to +"header" (but you might need to set it to "form" if, like Github for +instance, your OAuth2 provider doesn't like header authentication). In +fact, the `spring.oauth2.client.*` properties are bound to an instance +of `AuthorizationCodeResourceDetails` so all its properties can be +specified. + +=== Actuator Security + If the Actuator is also in use, you will find: * The management endpoints are secure even if the application endpoints are unsecure. 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 index 1a5fd5f639c..df61dd3519c 100644 --- 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 @@ -16,9 +16,7 @@ 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.boot.autoconfigure.SpringBootApplication; 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; @@ -95,9 +93,7 @@ import org.springframework.security.oauth2.config.annotation.web.configuration.E */ // @formatter:on -@Configuration -@ComponentScan -@EnableAutoConfiguration +@SpringBootApplication @EnableAuthorizationServer @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) 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 index abe9dc70644..5451b516cea 100644 --- 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 @@ -1,5 +1,14 @@ package sample; +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.request.MockMvcRequestBuilders.post; +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; + import java.util.Map; import org.junit.Before; @@ -21,14 +30,6 @@ 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 @@ -103,20 +104,20 @@ public class ApplicationTests { @Test public void useAppSecretsPlusUserAccountToGetBearerToken() throws Exception { + // @formatter:off MvcResult result = this.mvc .perform( - get("/oauth/token").// + post("/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()).// + "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(); + // @formatter:on Object accessToken = this.objectMapper.readValue( result.getResponse().getContentAsString(), Map.class).get("access_token");