SEC-2230: Add Header JavaConfig

Added JavaConfig for Headers. In the process, more HeaderWriter instances
were added so that we can reuse logic between the XML and JavaConfig. This
also prompted repackaging the writers.
This commit is contained in:
Rob Winch
2013-07-30 16:56:46 -05:00
parent 988e97e366
commit 606bddf598
38 changed files with 1038 additions and 156 deletions

View File

@@ -84,6 +84,14 @@ abstract class BaseSpringSpec extends Specification {
context.getBean("springSecurityFilterChain",Filter.class)
}
def getResponseHeaders() {
def headers = [:]
response.headerNames.each { name ->
headers.put(name, response.getHeaderValues(name).join(','))
}
return headers
}
AuthenticationManager authenticationManager() {
context.getBean(AuthenticationManager)
}

View File

@@ -66,6 +66,39 @@ class WebSecurityConfigurerAdapterTests extends BaseSpringSpec {
authenticationManager.messages.messageSource instanceof ApplicationContext
}
def "headers are populated by default"() {
setup: "load config that overrides http and accepts defaults"
loadConfig(HeadersArePopulatedByDefaultConfig)
request.secure = true
when: "invoke the springSecurityFilterChain"
springSecurityFilterChain.doFilter(request, response, chain)
then: "the default headers are added"
responseHeaders == ['X-Content-Type-Options':'nosniff',
'X-Frame-Options':'DENY',
'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains',
'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate',
'Pragma':'no-cache',
'X-XSS-Protection' : '1; mode=block']
}
@EnableWebSecurity
@Configuration
static class HeadersArePopulatedByDefaultConfig extends WebSecurityConfigurerAdapter {
@Override
protected void registerAuthentication(AuthenticationManagerBuilder auth)
throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
}
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
def "AuthenticationEventPublisher is registered for Web registerAuthentication"() {
when:
loadConfig(InMemoryAuthWithWebSecurityConfigurerAdapter)

View File

@@ -23,12 +23,12 @@ import org.springframework.mock.web.MockFilterChain
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.config.annotation.BaseSpringSpec
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
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.BaseWebConfig;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.WebSecurityConfigurer
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.BaseWebConfig
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.access.ExceptionTranslationFilter
@@ -37,9 +37,10 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.logout.LogoutFilter
import org.springframework.security.web.context.SecurityContextPersistenceFilter
import org.springframework.security.web.header.HeaderWriterFilter
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.session.SessionManagementFilter
import org.springframework.security.web.util.AnyRequestMatcher
/**
@@ -113,7 +114,7 @@ class DefaultFiltersTests extends BaseSpringSpec {
filterChains[0].filters.empty
filterChains[1].requestMatcher instanceof AnyRequestMatcher
filterChains[1].filters.collect { it.class } ==
[SecurityContextPersistenceFilter, LogoutFilter, RequestCacheAwareFilter,
[SecurityContextPersistenceFilter, HeaderWriterFilter, LogoutFilter, RequestCacheAwareFilter,
SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter,
ExceptionTranslationFilter, FilterSecurityInterceptor ]
}

View File

@@ -40,6 +40,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
import org.springframework.security.web.authentication.logout.LogoutFilter
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy
import org.springframework.security.web.context.SecurityContextPersistenceFilter
import org.springframework.security.web.header.HeaderWriterFilter
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
import org.springframework.security.web.session.SessionManagementFilter
@@ -62,7 +63,7 @@ class FormLoginConfigurerTests extends BaseSpringSpec {
filterChains[0].filters.empty
filterChains[1].requestMatcher instanceof AnyRequestMatcher
filterChains[1].filters.collect { it.class.name.contains('$') ? it.class.superclass : it.class } ==
[SecurityContextPersistenceFilter, LogoutFilter, UsernamePasswordAuthenticationFilter,
[SecurityContextPersistenceFilter, HeaderWriterFilter, LogoutFilter, UsernamePasswordAuthenticationFilter,
RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter,
AnonymousAuthenticationFilter, SessionManagementFilter, ExceptionTranslationFilter, FilterSecurityInterceptor ]

View File

@@ -0,0 +1,248 @@
/*
* Copyright 2002-2013 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.security.config.annotation.web.configurers;
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.BaseSpringSpec
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.BaseWebConfig
import org.springframework.security.web.header.writers.CacheControlHeadersWriter
import org.springframework.security.web.header.writers.HstsHeaderWriter
import org.springframework.security.web.header.writers.StaticHeadersWriter
import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter
import org.springframework.security.web.header.writers.frameoptions.StaticAllowFromStrategy
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode
import org.springframework.security.web.util.AnyRequestMatcher
/**
* Tests to verify that all the functionality of <headers> attributes is present
*
* @author Rob Winch
*
*/
public class NamespaceHttpHeadersTests extends BaseSpringSpec {
def "http/headers"() {
setup:
loadConfig(HeadersDefaultConfig)
request.secure = true
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['X-Content-Type-Options':'nosniff',
'X-Frame-Options':'DENY',
'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains',
'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate',
'Pragma':'no-cache',
'X-XSS-Protection' : '1; mode=block']
}
@Configuration
static class HeadersDefaultConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
}
}
def "http/headers/cache-control"() {
setup:
loadConfig(HeadersCacheControlConfig)
request.secure = true
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate',
'Pragma':'no-cache']
}
@Configuration
static class HeadersCacheControlConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
.addHeaderWriter(new CacheControlHeadersWriter())
}
}
def "http/headers/hsts"() {
setup:
loadConfig(HstsConfig)
request.secure = true
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains']
}
@Configuration
static class HstsConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
.addHeaderWriter(new HstsHeaderWriter())
}
}
def "http/headers/hsts custom"() {
setup:
loadConfig(HstsCustomConfig)
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['Strict-Transport-Security': 'max-age=15768000']
}
@Configuration
static class HstsCustomConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
// hsts@request-matcher-ref, hsts@max-age-seconds, hsts@include-subdomains
// Additional Constructors are provided to leverage default values
.addHeaderWriter(new HstsHeaderWriter(new AnyRequestMatcher(), 15768000, false))
}
}
def "http/headers/frame-options@policy=SAMEORIGIN"() {
setup:
loadConfig(FrameOptionsSameOriginConfig)
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['X-Frame-Options': 'SAMEORIGIN']
}
@Configuration
static class FrameOptionsSameOriginConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
// frame-options@policy=SAMEORIGIN
.addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN))
}
}
// frame-options@strategy, frame-options@value, frame-options@parameter are not provided instead use frame-options@ref
def "http/headers/frame-options"() {
setup:
loadConfig(FrameOptionsAllowFromConfig)
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['X-Frame-Options': 'ALLOW-FROM https://example.com']
}
@Configuration
static class FrameOptionsAllowFromConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
// frame-options@ref
.addHeaderWriter(new XFrameOptionsHeaderWriter(new StaticAllowFromStrategy(new URI("https://example.com"))))
}
}
def "http/headers/xss-protection"() {
setup:
loadConfig(XssProtectionConfig)
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['X-XSS-Protection': '1; mode=block']
}
@Configuration
static class XssProtectionConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
// xss-protection
.addHeaderWriter(new XXssProtectionHeaderWriter())
}
}
def "http/headers/xss-protection custom"() {
setup:
loadConfig(XssProtectionCustomConfig)
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['X-XSS-Protection': '1']
}
@Configuration
static class XssProtectionCustomConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
// xss-protection@enabled and xss-protection@block
.addHeaderWriter(new XXssProtectionHeaderWriter(enabled:true,block:false))
}
}
def "http/headers/content-type-options"() {
setup:
loadConfig(ContentTypeOptionsConfig)
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['X-Content-Type-Options': 'nosniff']
}
@Configuration
static class ContentTypeOptionsConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
// content-type-options
.addHeaderWriter(new XContentTypeOptionsHeaderWriter())
}
}
// header@name / header@value are not provided instead use header@ref
def "http/headers/header@ref"() {
setup:
loadConfig(HeaderRefConfig)
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['customHeaderName': 'customHeaderValue']
}
@Configuration
static class HeaderRefConfig extends BaseWebConfig {
@Override
protected void configure(HttpSecurity http) {
http
.headers()
.addHeaderWriter(new StaticHeadersWriter("customHeaderName", "customHeaderValue"))
}
}
}

View File

@@ -12,30 +12,15 @@
*/
package org.springframework.security.config.http
import org.springframework.security.util.FieldUtils
import javax.servlet.Filter
import javax.servlet.http.HttpServletRequest
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanCreationException
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
import org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException;
import org.springframework.mock.web.MockFilterChain
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.config.BeanIds
import org.springframework.security.openid.OpenIDAuthenticationFilter
import org.springframework.security.openid.OpenIDAuthenticationToken
import org.springframework.security.openid.OpenIDConsumer
import org.springframework.security.openid.OpenIDConsumerException
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.access.ExceptionTranslationFilter
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
import org.springframework.security.web.headers.HeadersFilter
import org.springframework.security.web.headers.StaticHeadersWriter;
import org.springframework.security.web.headers.frameoptions.StaticAllowFromStrategy;
import org.springframework.security.web.util.AnyRequestMatcher;
import org.springframework.security.web.FilterChainProxy
import org.springframework.security.web.header.HeaderWriterFilter
import org.springframework.security.web.header.writers.StaticHeadersWriter
import org.springframework.security.web.util.AnyRequestMatcher
/**
*
@@ -48,7 +33,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
expect:
!hf
@@ -61,7 +46,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
when:
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
then:
@@ -81,7 +66,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -97,7 +82,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -113,7 +98,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -129,7 +114,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -146,7 +131,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
then:
BeanDefinitionParsingException e = thrown()
@@ -162,7 +147,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
then:
BeanDefinitionParsingException e = thrown()
@@ -178,7 +163,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -195,7 +180,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -213,7 +198,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -234,7 +219,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
when:
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
then:
@@ -276,7 +261,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -293,7 +278,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -310,7 +295,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
@@ -327,11 +312,11 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
}
createAppContext()
def hf = getFilter(HeadersFilter)
def hf = getFilter(HeaderWriterFilter)
then:
BeanDefinitionParsingException e = thrown()
e.message.contains '<xss-protection enabled="false"/> does not allow block="true".'
BeanCreationException e = thrown()
e.message.contains 'Cannot set block to true with enabled false'
}
def 'http headers cache-control'() {