Commit af320b49 authored by Dave Syer's avatar Dave Syer

Rationalize some features and merge in customizers from Spring Cloud

parent 5468949a
......@@ -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 {
protected abstract static class BaseConfiguration {
@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 oauth2RemoteResource() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
return details;
}
}
@Configuration
@ConditionalOnNotWebApplication
protected static class SingletonScopedConfiguration {
@Bean
@ConfigurationProperties("spring.oauth2.client")
@Primary
public AuthorizationCodeResourceDetails authorizationCodeResourceDetails() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientSecret(this.credentials.getClientSecret());
details.setClientId(this.credentials.getClientId());
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
.<ClientHttpRequestInterceptor> 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<String, String> 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 <code>@EnableOAuth2Client</code>
*/
@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;
}
}
......
/*
* 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);
}
......@@ -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;
}
......
......@@ -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("<N/A>");
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<UserInfoRestTemplateCustomizer> 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
.<ClientHttpRequestInterceptor> 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<String, String> 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<String, OAuth2RestOperations> 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<String, OAuth2RestOperations> 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<JwtAccessTokenConverterConfigurer> 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<Void> requestEntity = new HttpEntity<Void>(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;
}
......
/*
* 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 <i>only</i> 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);
}
......@@ -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<OAuth2RestOperations> 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<String, OAuth2RestOperations> resources) {
this.resources = new ArrayList<OAuth2RestOperations>();
for (Entry<String, OAuth2RestOperations> 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<String, Object> 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<String, Object> 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")
......
......@@ -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,\
......
......@@ -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 <code>@Configuration</code> classes
* @author Andy Wilkinson
*/
public class AutoConfigureConfigurationClassTests extends AbstractConfigurationClassTests {
......
......@@ -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
......
......@@ -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());
}
......
......@@ -136,7 +136,7 @@
<spring-social-linkedin.version>1.0.1.RELEASE</spring-social-linkedin.version>
<spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version>
<spring-security.version>3.2.5.RELEASE</spring-security.version>
<spring-security-oauth2.version>2.0.5.RELEASE</spring-security-oauth2.version>
<spring-security-oauth2.version>2.0.7.RELEASE</spring-security-oauth2.version>
<spring-security-jwt.version>1.0.2.RELEASE</spring-security-jwt.version>
<spring-ws.version>2.2.1.RELEASE</spring-ws.version>
<statsd-client.version>3.1.0</statsd-client.version>
......
......@@ -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.
......
......@@ -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)
......
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");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment