Add support for OAuth relay in zuul proxy

This commit is contained in:
Dave Syer
2014-08-27 18:52:59 +01:00
parent 2cdea95fd1
commit adaece0889
10 changed files with 328 additions and 8 deletions

View File

@@ -72,6 +72,11 @@
<artifactId>eureka-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -0,0 +1,41 @@
/*
* 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.platform.cloudfoundry.proxy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import com.netflix.zuul.ZuulFilter;
/**
* @author Dave Syer
*
*/
@Configuration
@ConditionalOnClass({ ZuulFilter.class, EnableOAuth2Client.class, SecurityProperties.class })
@ConditionalOnWebApplication
public class CloudfoundryProxyConfiguration {
@Bean
public CloudfoundryTokenFilter cloudfoundryTokenFilter() {
return new CloudfoundryTokenFilter();
}
}

View File

@@ -0,0 +1,46 @@
package org.springframework.platform.cloudfoundry.proxy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
public class CloudfoundryTokenFilter extends ZuulFilter {
private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
@Override
public int filterOrder() {
return 10;
}
@Override
public String filterType() {
return "pre";
}
@Override
public boolean shouldFilter() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof OAuth2Authentication) {
Object details = auth.getDetails();
if (details instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails oauth = (OAuth2AuthenticationDetails) details;
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set(ACCESS_TOKEN, oauth.getTokenValue());
return true;
}
}
return false;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader("authorization", "Bearer " + ctx.get(ACCESS_TOKEN));
return null;
}
}

View File

@@ -0,0 +1,126 @@
/*
* 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.platform.cloudfoundry.resource;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties;
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.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.ClassUtils;
/**
* @author Dave Syer
*
*/
@Configuration
@ConditionalOnExpression("'${cloudfoundry.resource.clientId:${vcap.services.resource.credentials.clientId:}}'!=''")
@ConditionalOnClass({ EnableResourceServer.class, SecurityProperties.class })
@ConditionalOnWebApplication
@EnableResourceServer
@EnableConfigurationProperties(CloudfoundryResourceProperties.class)
public class CloudfoundryResourceConfiguration {
@Autowired
private CloudfoundryResourceProperties resource;
@Bean
@ConditionalOnMissingBean(ResourceServerConfigurer.class)
public ResourceServerConfigurer resourceServer() {
return new ResourceSecurityConfigurer(resource);
}
@Bean
@ConditionalOnMissingBean(ResourceServerTokenServices.class)
public ResourceServerTokenServices tokenServices() {
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl(resource.getTokenInfoUri());
services.setClientId(resource.getClientId());
services.setClientSecret(resource.getClientSecret());
return services;
}
protected static class ResourceSecurityConfigurer extends ResourceServerConfigurerAdapter {
private CloudfoundryResourceProperties resource;
@Autowired
public ResourceSecurityConfigurer(CloudfoundryResourceProperties resource) {
this.resource = resource;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources)
throws Exception {
resources.resourceId(resource.getClientId());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
@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() {
if (ClassUtils
.isPresent(
"org.springframework.boot.actuate.autoconfigure.ManagementServerProperties",
null)) {
return ManagementServerProperties.ACCESS_OVERRIDE_ORDER - 10;
}
return SecurityProperties.ACCESS_OVERRIDE_ORDER - 10;
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.platform.cloudfoundry.resource;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
/**
* @author Dave Syer
*
*/
@ConfigurationProperties("cloudfoundry.resource")
@Data
public class CloudfoundryResourceProperties implements Validator {
private String serviceId = "resource";
@Value("${vcap.services.${cloudfoundry.resource.serviceId:resource}.credentials.clientId:}")
private String clientId;
@Value("${vcap.services.${cloudfoundry.resource.serviceId:resource}.credentials.clientSecret:}")
private String clientSecret;
@Value("${vcap.services.${cloudfoundry.resource.serviceId:resource}.credentials.tokenInfoUri:}")
private String tokenInfoUri;
@Override
public boolean supports(Class<?> clazz) {
return CloudfoundryResourceProperties.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
CloudfoundryResourceProperties resource = (CloudfoundryResourceProperties) target;
if (StringUtils.hasText(resource.getClientId())) {
if (!StringUtils.hasText(resource.getTokenInfoUri())) {
errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri", "Missing tokenInfoUri");
}
if (!StringUtils.hasText(resource.getClientSecret())) {
errors.rejectValue("clientSecret", "missing.clientSecret", "Missing clientSecret");
}
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.platform.cloudfoundry.resource;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* @author Dave Syer
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CloudfoundryResourceConfiguration.class)
public @interface EnableCloudfoundryResource {
}

View File

@@ -63,7 +63,7 @@ import org.springframework.util.ClassUtils;
*
*/
@Configuration
@ConditionalOnExpression("'${cloudfoundry.sso.tokenUri:${vcap.services.sso.credentials.tokenUri:}}'!=''")
@ConditionalOnExpression("'${cloudfoundry.sso.clientId:${vcap.services.sso.credentials.clientId:}}'!=''")
@ConditionalOnClass({ EnableOAuth2Client.class, SecurityProperties.class })
@ConditionalOnWebApplication
@EnableOAuth2Client

View File

@@ -71,12 +71,12 @@ public class CloudfoundrySsoProperties implements Validator {
@Override
public void validate(Object target, Errors errors) {
CloudfoundrySsoProperties sso = (CloudfoundrySsoProperties) target;
if (StringUtils.hasText(sso.getTokenUri())) {
if (StringUtils.hasText(sso.getClientId())) {
if (!StringUtils.hasText(sso.getAuthorizationUri())) {
errors.rejectValue("authorizeUri", "missing.authorizeUri", "Missing authorizeUri");
}
if (!StringUtils.hasText(sso.getClientId())) {
errors.rejectValue("clientId", "missing.clientId", "Missing clientId");
if (!StringUtils.hasText(sso.getTokenUri())) {
errors.rejectValue("tokenUri", "missing.tokenUri", "Missing tokenUri");
}
if (!StringUtils.hasText(sso.getClientSecret())) {
errors.rejectValue("clientSecret", "missing.clientSecret", "Missing clientSecret");

View File

@@ -1,2 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.platform.cloudfoundry.broker.configuration.ServiceBrokerAutoConfiguration
org.springframework.platform.cloudfoundry.broker.configuration.ServiceBrokerAutoConfiguration,\
org.springframework.platform.cloudfoundry.proxy.CloudfoundryProxyConfiguration

View File

@@ -33,16 +33,18 @@ import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port=0")
@IntegrationTest({ "server.port=0", "security.basic.enabled=false",
"cloudfoundry.sso.clientId=", "cloudfoundry.resource.clientId=" })
public class ApplicationTests {
@Value("${local.server.port}")
private int port = 0;
@Test
public void catalogLoads() {
@SuppressWarnings("rawtypes")
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity("http://localhost:" + port + "/v2/catalog", Map.class);
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
"http://localhost:" + port + "/v2/catalog", Map.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
}