/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.servlet.Filter;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
/**
* Tests for {@link SpringBootWebSecurityConfiguration}.
*
* @author Dave Syer
* @author Rob Winch
* @author Andy Wilkinson
*/
public class SpringBootWebSecurityConfigurationTests {
private ConfigurableApplicationContext context;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void testWebConfigurationOverrideGlobalAuthentication() throws Exception {
this.context = SpringApplication.run(TestWebConfiguration.class,
"--server.port=0");
assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull();
assertThat(this.context.getBean(AuthenticationManager.class)
.authenticate(new UsernamePasswordAuthenticationToken("dave", "secret")))
.isNotNull();
}
@Test
public void testWebConfigurationFilterChainUnauthenticated() throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--server.port=0");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters(
this.context.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andExpect(MockMvcResultMatchers.header().string("www-authenticate",
Matchers.containsString("realm=\"Spring\"")));
}
@Test
public void testWebConfigurationFilterChainUnauthenticatedWithAuthorizeModeNone()
throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--server.port=0", "--security.basic.authorize-mode=none");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters(
this.context.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isNotFound());
}
@Test
public void testWebConfigurationFilterChainUnauthenticatedWithAuthorizeModeAuthenticated()
throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--server.port=0", "--security.basic.authorize-mode=authenticated");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters(
this.context.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andExpect(MockMvcResultMatchers.header().string("www-authenticate",
Matchers.containsString("realm=\"Spring\"")));
}
@Test
public void testWebConfigurationFilterChainBadCredentials() throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--server.port=0");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters(
this.context.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(
MockMvcRequestBuilders.get("/").header("authorization", "Basic xxx"))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andExpect(MockMvcResultMatchers.header().string("www-authenticate",
Matchers.containsString("realm=\"Spring\"")));
}
@Test
public void testWebConfigurationInjectGlobalAuthentication() throws Exception {
this.context = SpringApplication.run(TestInjectWebConfiguration.class,
"--server.port=0");
assertThat(this.context.getBean(AuthenticationManagerBuilder.class)).isNotNull();
assertThat(this.context.getBean(AuthenticationManager.class)
.authenticate(new UsernamePasswordAuthenticationToken("dave", "secret")))
.isNotNull();
}
// gh-3447
@Test
public void testHiddenHttpMethodFilterOrderedFirst() throws Exception {
this.context = SpringApplication.run(DenyPostRequestConfig.class,
"--server.port=0");
int port = Integer
.parseInt(this.context.getEnvironment().getProperty("local.server.port"));
TestRestTemplate rest = new TestRestTemplate();
// not overriding causes forbidden
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
ResponseEntity<Object> result = rest
.postForEntity("http://localhost:" + port + "/", form, Object.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
// override method with GET
form = new LinkedMultiValueMap<>();
form.add("_method", "GET");
result = rest.postForEntity("http://localhost:" + port + "/", form, Object.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void defaultHeaderConfiguration() throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--server.port=0");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters((FilterChainProxy) this.context
.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.header().string("X-Content-Type-Options",
is(notNullValue())))
.andExpect(MockMvcResultMatchers.header().string("X-XSS-Protection",
is(notNullValue())))
.andExpect(MockMvcResultMatchers.header().string("Cache-Control",
is(notNullValue())))
.andExpect(MockMvcResultMatchers.header().string("X-Frame-Options",
is(notNullValue())))
.andExpect(MockMvcResultMatchers.header()
.doesNotExist("Content-Security-Policy"));
}
@Test
public void securityHeadersCanBeDisabled() throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--server.port=0", "--security.headers.content-type=false",
"--security.headers.xss=false", "--security.headers.cache=false",
"--security.headers.frame=false");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters(
this.context.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andExpect(MockMvcResultMatchers.header()
.doesNotExist("X-Content-Type-Options"))
.andExpect(
MockMvcResultMatchers.header().doesNotExist("X-XSS-Protection"))
.andExpect(MockMvcResultMatchers.header().doesNotExist("Cache-Control"))
.andExpect(
MockMvcResultMatchers.header().doesNotExist("X-Frame-Options"));
}
@Test
public void contentSecurityPolicyConfiguration() throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--security.headers.content-security-policy=default-src 'self';",
"--server.port=0");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters((FilterChainProxy) this.context
.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.header()
.string("Content-Security-Policy", is("default-src 'self';")))
.andExpect(MockMvcResultMatchers.header()
.doesNotExist("Content-Security-Policy-Report-Only"));
}
@Test
public void contentSecurityPolicyReportOnlyConfiguration() throws Exception {
this.context = SpringApplication.run(VanillaWebConfiguration.class,
"--security.headers.content-security-policy=default-src 'self';",
"--security.headers.content-security-policy-mode=report-only",
"--server.port=0");
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup((WebApplicationContext) this.context)
.addFilters((FilterChainProxy) this.context
.getBean("springSecurityFilterChain", Filter.class))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.header().string(
"Content-Security-Policy-Report-Only", is("default-src 'self';")))
.andExpect(MockMvcResultMatchers.header()
.doesNotExist("Content-Security-Policy"));
}
@Configuration
@Import(TestWebConfiguration.class)
@Order(Ordered.LOWEST_PRECEDENCE)
protected static class TestInjectWebConfiguration
extends WebSecurityConfigurerAdapter {
private final AuthenticationManagerBuilder auth;
// It's a bad idea to inject an AuthenticationManager into a
// WebSecurityConfigurerAdapter because it can cascade early instantiation,
// unless you explicitly want the Boot default AuthenticationManager. It's
// better to inject the builder, if you want the global AuthenticationManager. It
// might even be necessary to wrap the builder in a lazy AuthenticationManager
// (that calls getOrBuild() only when the AuthenticationManager is actually
// called).
protected TestInjectWebConfiguration(AuthenticationManagerBuilder auth) {
this.auth = auth;
}
@Override
public void init(WebSecurity web) throws Exception {
this.auth.getOrBuild();
}
}
@MinimalWebConfiguration
@Import(SecurityAutoConfiguration.class)
protected static class VanillaWebConfiguration {
}
@MinimalWebConfiguration
@Import(SecurityAutoConfiguration.class)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class TestWebConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("dave").password("secret")
.roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().denyAll();
}
}
@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ ServletWebServerFactoryAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
@MinimalWebConfiguration
@Import(SecurityAutoConfiguration.class)
protected static class DenyPostRequestConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.POST, "/**").denyAll();
}
}
}