diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java index 523906ab2a4..c98c46c3eab 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerConfiguration.java @@ -17,6 +17,9 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource; import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -45,6 +48,7 @@ import org.springframework.security.oauth2.config.annotation.web.configuration.R 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.stereotype.Component; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -98,6 +102,36 @@ public class OAuth2ResourceServerConfiguration { } + @Component + public static class ResourceServerFilterChainOrderProcessor + implements BeanPostProcessor { + + private ResourceServerProperties resource; + + @Autowired + public ResourceServerFilterChainOrderProcessor( + ResourceServerProperties resource) { + this.resource = resource; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof ResourceServerConfiguration) { + ResourceServerConfiguration config = (ResourceServerConfiguration) bean; + config.setOrder(this.resource.getFilterOrder()); + } + return bean; + } + + } + protected static class ResourceServerCondition extends SpringBootCondition implements ConfigurationCondition { 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 056f73a3f3f..85fb6512beb 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 @@ -23,6 +23,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; @@ -77,6 +78,12 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware { private Jwt jwt = new Jwt(); + /** + * The order of the filter chain used to authenticate tokens. Default puts it after + * the actuator endpoints and before the default HTTP basic filter chain (catchall). + */ + private int filterOrder = SecurityProperties.ACCESS_OVERRIDE_ORDER - 1; + public ResourceServerProperties() { this(null, null); } @@ -159,6 +166,14 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware { return this.clientSecret; } + public int getFilterOrder() { + return this.filterOrder; + } + + public void setFilterOrder(int filterOrder) { + this.filterOrder = filterOrder; + } + @Override public boolean supports(Class clazz) { return ResourceServerProperties.class.isAssignableFrom(clazz); diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java index 22bad3fbccf..eda0126ad14 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/OAuth2AutoConfigurationTests.java @@ -21,11 +21,13 @@ import java.util.Arrays; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; + import org.junit.Test; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.oauth2.authserver.OAuth2AuthorizationServerConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.method.OAuth2MethodSecurityConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerConfiguration; @@ -43,6 +45,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -437,6 +440,7 @@ public class OAuth2AutoConfigurationTests { } @Configuration + @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) protected static class TestSecurityConfiguration extends WebSecurityConfigurerAdapter { diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 3a0217bcf61..75dc0f272fe 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -1,6 +1,4 @@ - - + 4.0.0 org.springframework.boot spring-boot-dependencies @@ -2563,4 +2561,4 @@ integration-test - + \ No newline at end of file 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 1fc9851b7c1..cc011c54f63 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -2528,6 +2528,10 @@ WARNING: If you use the `security.oauth2.resource.jwt.key-uri` the authorization 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. +OAuth2 resources are protected by a filter chain with order +`security.oauth2.resource.filter-order` and the default is after the +filter protecting the actuator endpoints by default (so actuator +endpoints will stay on HTTP Basic unless you change the order). [[boot-features-security-oauth2-token-type]] diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 9f10e354c6a..6a07c185339 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -75,6 +75,7 @@ spring-boot-sample-property-validation spring-boot-sample-secure spring-boot-sample-secure-oauth2 + spring-boot-sample-secure-oauth2-actuator spring-boot-sample-secure-oauth2-resource spring-boot-sample-servlet spring-boot-sample-session-redis diff --git a/spring-boot-samples/spring-boot-sample-ant/pom.xml b/spring-boot-samples/spring-boot-sample-ant/pom.xml index 5c474c48fa1..3fa53b05d46 100644 --- a/spring-boot-samples/spring-boot-sample-ant/pom.xml +++ b/spring-boot-samples/spring-boot-sample-ant/pom.xml @@ -102,4 +102,4 @@ - + \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/pom.xml b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/pom.xml new file mode 100644 index 00000000000..8574cd3e629 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.5.0.BUILD-SNAPSHOT + + spring-boot-sample-secure-oauth2-actuator + spring-boot-sample-secure-oauth2-actuator + Spring Boot Security OAuth2 Actuator Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.security.oauth + spring-security-oauth2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/resource/SampleSecureOAuth2ResourceApplication.java b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/resource/SampleSecureOAuth2ResourceApplication.java new file mode 100644 index 00000000000..761d5546b8c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/java/sample/secure/oauth2/resource/SampleSecureOAuth2ResourceApplication.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.secure.oauth2.resource; + +import java.util.UUID; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@EnableResourceServer +@RestController +public class SampleSecureOAuth2ResourceApplication { + + @GetMapping("/") + public Message home() { + return new Message("Hello World"); + } + + public static void main(String[] args) { + SpringApplication.run(SampleSecureOAuth2ResourceApplication.class, args); + } + +} + +class Message { + + private String id = UUID.randomUUID().toString(); + + private String value; + + public Message(String value) { + this.value = value; + } + + public String getId() { + return id; + } + + public String getValue() { + return value; + } + +} \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/resources/application.properties new file mode 100644 index 00000000000..ac60cbc595d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/main/resources/application.properties @@ -0,0 +1,6 @@ +server.port=8081 +security.basic.enabled=true +security.user.password=password +security.oauth2.resource.id=service +security.oauth2.resource.userInfoUri=http://localhost:8080/user +logging.level.org.springframework.security=DEBUG diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/test/java/sample/secure/oauth2/resource/SampleSecureOAuth2ResourceApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/test/java/sample/secure/oauth2/resource/SampleSecureOAuth2ResourceApplicationTests.java new file mode 100644 index 00000000000..74bf07a735e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-secure-oauth2-actuator/src/test/java/sample/secure/oauth2/resource/SampleSecureOAuth2ResourceApplicationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.secure.oauth2.resource; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.Base64Utils; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +/** + * Series of automated integration tests to verify proper behavior of auto-configured, + * OAuth2-secured system + * + * @author Dave Syer + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class SampleSecureOAuth2ResourceApplicationTests { + + @Autowired + WebApplicationContext context; + + @Autowired + FilterChainProxy filterChain; + + private MockMvc mvc; + + @Before + public void setUp() { + this.mvc = webAppContextSetup(this.context).addFilters(this.filterChain).build(); + SecurityContextHolder.clearContext(); + } + + @Test + public void homePageSecuredByDefault() throws Exception { + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()) + .andExpect(header().string("WWW-Authenticate", containsString("Bearer"))) + .andDo(print()); + } + + @Test + public void healthAvailable() throws Exception { + this.mvc.perform(get("/health")).andExpect(status().isOk()).andDo(print()); + } + + @Test + public void envSecuredWithBasic() throws Exception { + this.mvc.perform(get("/env")).andExpect(status().isUnauthorized()) + .andExpect(header().string("WWW-Authenticate", containsString("Basic"))) + .andDo(print()); + } + + @Test + public void envWithPassword() throws Exception { + this.mvc.perform(get("/env").header("Authorization", + "Basic " + Base64Utils.encodeToString("user:password".getBytes()))) + .andExpect(status().isOk()).andDo(print()); + } + +}