diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java index 3efc60f7d3..67f71a7ad9 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.java @@ -54,4 +54,5 @@ public abstract class Elements { public static final String LDAP_PASSWORD_COMPARE = "password-compare"; public static final String DEBUG = "debug"; public static final String HTTP_FIREWALL = "http-firewall"; + public static final String ADD_HEADERS = "add-headers"; } diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java new file mode 100644 index 0000000000..ce7d228f1c --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2012 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.http; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.web.headers.HeadersFilter; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Parser for the {@code HeadersFilter}. + * + * @author Marten Deinum + * @since 3.2 + */ +public class HeadersBeanDefinitionParser implements BeanDefinitionParser { + + private static final String ATT_ENABLED = "enabled"; + private static final String ATT_BLOCK = "block"; + + private static final String ATT_POLICY = "policy"; + private static final String ATT_ORIGIN = "origin"; + + private static final String ATT_NAME = "name"; + private static final String ATT_VALUE = "value"; + + private static final String XSS_ELEMENT = "xss-protection"; + private static final String CONTENT_TYPE_ELEMENT = "content-type-options"; + private static final String FRAME_OPTIONS_ELEMENT = "frame-options"; + private static final String GENERIC_HEADER_ELEMENT = "header"; + + private static final String XSS_PROTECTION_HEADER = "X-XSS-Protection"; + private static final String FRAME_OPTIONS_HEADER = "X-Frame-Options"; + private static final String CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options"; + + private static final String ALLOW_FROM = "ALLOW-FROM"; + + public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(HeadersFilter.class); + final Map headers = new HashMap(); + + parseXssElement(element, headers); + parseFrameOptionsElement(element, parserContext, headers); + parseContentTypeOptionsElement(element, headers); + + parseHeaderElements(element, headers); + + builder.addPropertyValue("headers", headers); + return builder.getBeanDefinition(); + } + + private void parseHeaderElements(Element element, Map headers) { + List headerEtls = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT); + for (Element headerEtl : headerEtls) { + headers.put(headerEtl.getAttribute(ATT_NAME), headerEtl.getAttribute(ATT_VALUE)); + } + } + + private void parseContentTypeOptionsElement(Element element, Map headers) { + Element contentTypeElt = DomUtils.getChildElementByTagName(element, CONTENT_TYPE_ELEMENT); + if (contentTypeElt != null) { + headers.put(CONTENT_TYPE_OPTIONS_HEADER, "nosniff"); + } + } + + private void parseFrameOptionsElement(Element element, ParserContext parserContext, Map headers) { + Element frameElt = DomUtils.getChildElementByTagName(element, FRAME_OPTIONS_ELEMENT); + if (frameElt != null) { + String header = getAttribute(frameElt, ATT_POLICY, "DENY"); + if (ALLOW_FROM.equals(header) ) { + String origin = frameElt.getAttribute(ATT_ORIGIN); + if (!StringUtils.hasText(origin) ) { + parserContext.getReaderContext().error("Frame options header value ALLOW-FROM required an origin to be specified.", frameElt); + } + header += " " + origin; + } + headers.put(FRAME_OPTIONS_HEADER, header); + } + } + + private void parseXssElement(Element element, Map headers) { + Element xssElt = DomUtils.getChildElementByTagName(element, XSS_ELEMENT); + if (xssElt != null) { + boolean enabled = Boolean.valueOf(getAttribute(xssElt, ATT_ENABLED, "true")); + boolean block = Boolean.valueOf(getAttribute(xssElt, ATT_BLOCK, "true")); + + String value = enabled ? "1" : "0"; + if (enabled && block) { + value += "; mode=block"; + } + headers.put(XSS_PROTECTION_HEADER, value); + } + } + + private String getAttribute(Element element, String name, String defaultValue) { + String value = element.getAttribute(name); + if (StringUtils.hasText(value)) { + return value; + } else { + return defaultValue; + } + } +} diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index a568307d4e..65fca17370 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -117,6 +117,7 @@ class HttpConfigurationBuilder { private final BeanReference portResolver; private BeanReference fsi; private BeanReference requestCache; + private BeanDefinition addHeadersFilter; public HttpConfigurationBuilder(Element element, ParserContext pc, BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) { @@ -151,6 +152,7 @@ class HttpConfigurationBuilder { createJaasApiFilter(); createChannelProcessingFilter(); createFilterSecurityInterceptor(authenticationManager); + createAddHeadersFilter(); } @SuppressWarnings("rawtypes") @@ -554,6 +556,14 @@ class HttpConfigurationBuilder { this.fsi = new RuntimeBeanReference(fsiId); } + private void createAddHeadersFilter() { + Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.ADD_HEADERS); + if (elmt != null) { + this.addHeadersFilter = new HeadersBeanDefinitionParser().parse(elmt, pc); + } + + } + BeanReference getSessionStrategy() { return sessionStrategyRef; } @@ -601,6 +611,10 @@ class HttpConfigurationBuilder { filters.add(new OrderDecorator(requestCacheAwareFilter, REQUEST_CACHE_FILTER)); } + if (addHeadersFilter != null) { + filters.add(new OrderDecorator(addHeadersFilter, HEADERS_FILTER)); + } + return filters; } } diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index 58a9bc491a..20194ecedb 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -29,6 +29,7 @@ enum SecurityFilters { CONCURRENT_SESSION_FILTER, /** {@link WebAsyncManagerIntegrationFilter} */ WEB_ASYNC_MANAGER_FILTER, + HEADERS_FILTER, LOGOUT_FILTER, X509_FILTER, PRE_AUTH_FILTER, diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc index 6d55f40585..7cfa8c16c3 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc @@ -281,7 +281,7 @@ http-firewall = http = ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "secured" attribute to "false". - element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler?) } + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers?) } http.attlist &= ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. attribute pattern {xsd:token}? @@ -718,6 +718,43 @@ jdbc-user-service.attlist &= jdbc-user-service.attlist &= role-prefix? +headers = + ## Element for configuration of the AddHeadersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + element headers {xss-protection? & frame-options? & content-type-options? & header*} + +frame-options = + ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. + element frame-options {frame-options.attlist,empty} +frame-options.attlist &= + ## Specify the policy to use for the X-Frame-Options-Header. + attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? +frame-options.attlist &= + ## Specify the origin to use when ALLOW-FROM is chosen. + attribute origin {xsd:token}? + +xss-protection = + ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. + element xss-protection {xss-protection.attlist,empty} +xss-protection.attlist &= + ## enable or disable the X-XSS-Protection header. Default is 'true' meaning it is enabled. + attribute enabled {xsd:boolean}? +xss-protection.attlist &= + ## Add mode=block to the header or not, default is on. + attribute block {xsd:boolean}? + +content-type-options = + ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + element content-type-options {empty} + +header= + ## Add additional headers to the response. + element header {header.attlist} +header.attlist &= + ## The name of the header to add. + attribute name {xsd:token} +header.attlist &= + ## The value for the header. + attribute value {xsd:token} any-user-service = user-service | jdbc-user-service | ldap-user-service diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd index 30319e6412..c9c232588b 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd @@ -1024,6 +1024,7 @@ + @@ -2231,6 +2232,106 @@ + + + Element for configuration of the AddHeadersFilter. Enables easy setting for the + X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + + + + + + + + + + + + + + Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options + header. + + + + + + + + + + Specify the policy to use for the X-Frame-Options-Header. + + + + + + + + + + + + + Specify the origin to use when ALLOW-FROM is chosen. + + + + + + + Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the + X-XSS-Protection header. + + + + + + + + + + enable or disable the X-XSS-Protection header. Default is 'true' meaning it is enabled. + + + + + + Add mode=block to the header or not, default is on. + + + + + + + Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + + + + + + + Add additional headers to the response. + + + + + + + + + + The name of the header to add. + + + + + + The value for the header. + + + + diff --git a/config/src/main/resources/org/springframework/security/config/spring-security.xsl b/config/src/main/resources/org/springframework/security/config/spring-security.xsl index 2b090d2dfd..46b9e89acc 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security.xsl +++ b/config/src/main/resources/org/springframework/security/config/spring-security.xsl @@ -9,7 +9,7 @@ - ,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509, + ,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509,add-headers, diff --git a/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java b/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java index 566e3803e2..9b65a7f3ae 100644 --- a/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/SecurityNamespaceHandlerTests.java @@ -48,7 +48,7 @@ public class SecurityNamespaceHandlerTests { } @Test - public void pre31SchemaAreNotSupported() throws Exception { + public void pre32SchemaAreNotSupported() throws Exception { try { new InMemoryXmlApplicationContext( "" + @@ -57,7 +57,7 @@ public class SecurityNamespaceHandlerTests { ); fail("Expected BeanDefinitionParsingException"); } catch (BeanDefinitionParsingException expected) { - assertTrue(expected.getMessage().contains("You cannot use a spring-security-2.0.xsd or")); + assertTrue(expected.getMessage().contains("You cannot use a spring-security-2.0.xsd")); } } diff --git a/docs/manual/src/docbook/appendix-namespace.xml b/docs/manual/src/docbook/appendix-namespace.xml index fd11359142..a9e41271d8 100644 --- a/docs/manual/src/docbook/appendix-namespace.xml +++ b/docs/manual/src/docbook/appendix-namespace.xml @@ -15,7 +15,7 @@ explaining their purpose. The namespace is written in RELAX NG Compact format and later converted into an XSD schema. If you are familiar with this format, you may wish to examine the schema file directly.
Web Application Security @@ -210,6 +210,7 @@ Child Elements of <http> access-denied-handler + headers anonymous custom-filter expression-handler @@ -224,6 +225,7 @@ request-cache session-management x509 + headers
@@ -257,6 +259,131 @@ +
+ <literal><headers></literal> + This element allows for configuring additional (security) headers to be send with the response. + It enables easy configuration for several headers and also allows for setting custom headers through + the header element. + + X-Frame-Options - Can be set using the + frame-options element. The + X-Frame-Options + header can be used to prevent clickjacking attacks. + X-XSS-Protection - Can be set using the + xss-protection element. + The X-XSS-Protection + header can be used by browser to do basic control. + X-Content-Type-Options - Can be set using the + content-type-options element. The + X-Content-Type-Options header prevents Internet Explorer from + MIME-sniffing a response away from the declared content-type. This also applies to Google + Chrome, when downloading extensions. + + +
+ Parent Elements of <literal><headers></literal> + + http + +
+
+ Child Elements of <literal><headers></literal> + + content-type-options + frame-options + header + xss-protection + +
+
+
+ <literal><frame-options></literal> + When enabled adds the X-Frame-Options header to the response, this allows newer browsers to do some security + checks and prevent clickjacking attacks. +
+ <literal><frame-options></literal> Attributes +
+ <literal>frame-options-policy</literal> + + + DENY The page cannot be displayed in a frame, regardless of + the site attempting to do so. + SAMEORIGIN The page can only be displayed in a frame on the + same origin as the page itself + ALLOW-FROM origin + The page can only be displayed in a frame on the specified origin. + + + In other words, if you specify DENY, not only will attempts to load the page in a frame fail + when loaded from other sites, attempts to do so will fail when loaded from the same site. On the + other hand, if you specify SAMEORIGIN, you can still use the page in a frame as long as the site + including it in a frame it is the same as the one serving the page. + +
+
+ <literal>frame-options-origin</literal> + The origin +
+
+
+ Parent Elements of <literal><frame-options></literal> + + headers + +
+
+
+ <literal><xss-protection></literal> + Adds the X-XSS-Protection header to the response. This is in no-way a full protection to XSS attacks! +
+
+ <literal>xss-protection-enabled</literal> + Enable or Disable xss-protection. +
+
+ <literal>xss-protection-block</literal> + When enabled adds mode=block to the header. Which indicates to the browser that loading should be blocked. +
+
+
+ Parent Elements of <literal><xss-protection></literal> + + headers + +
+
+
+ <literal><content-type-options></literal> + Add the X-Content-Type-Options header to the response. Indicates the browser (IE8+) to enable detection + for MIME-sniffing. +
+ Parent Elements of <literal><content-type-options></literal> + + headers + +
+
+
+ <literal><header></literal> + Add additional headers to the response, both the name and value need to be specified. +
+ <literal><header-attributes></literal> Attributes +
+ <literal>header-name</literal> + The name of the header. +
+
+ <literal>header-value</literal> + The value of the header to add. +
+
+
+ Parent Elements of <literal><header></literal> + + headers + +
+
<literal><anonymous></literal> Adds an AnonymousAuthenticationFilter to the stack and an diff --git a/docs/manual/src/docbook/namespace-config.xml b/docs/manual/src/docbook/namespace-config.xml index 892748b8f0..81e11adb5b 100644 --- a/docs/manual/src/docbook/namespace-config.xml +++ b/docs/manual/src/docbook/namespace-config.xml @@ -609,6 +609,23 @@ List<OpenIDAttribute> attributes = token.getAttributes();The MyOpenID providers.
+
+ Response Headers + A lot of different attacks to hijack content, sessions or connections are available and lately + browsers (optionally) can help to prevent those attacks. To enable these features we need to send some + additional headers to the client. Spring Security allows for easy configuration for several headers. + + + + + +
+ + ]]> + + +
Adding in Your Own Filters If you've used Spring Security before, you'll know that the framework maintains a @@ -663,6 +680,11 @@ List<OpenIDAttribute> attributes = token.getAttributes();The ConcurrentSessionFilter session-management/concurrency-control + + HEADERS_FILTER + HeadersFilter + http/headers + LOGOUT_FILTER LogoutFilter diff --git a/samples/contacts/src/main/resources/applicationContext-security.xml b/samples/contacts/src/main/resources/applicationContext-security.xml index 31a43d5f4c..2a02821eb1 100644 --- a/samples/contacts/src/main/resources/applicationContext-security.xml +++ b/samples/contacts/src/main/resources/applicationContext-security.xml @@ -30,6 +30,12 @@ + + + + +
+ diff --git a/web/src/main/java/org/springframework/security/web/headers/HeadersFilter.java b/web/src/main/java/org/springframework/security/web/headers/HeadersFilter.java new file mode 100644 index 0000000000..4863d85046 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/headers/HeadersFilter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2012 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.web.headers; + +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Filter implementation to add headers to the current request. Can be useful to add certain headers which enable + * browser protection. Like X-Frame-Options, X-XSS-Protection and X-Content-Type-Options. + * + * @author Marten Deinum + * @since 3.2 + * + */ +public class HeadersFilter extends OncePerRequestFilter { + + /** Map of headers to add to a response */ + private final Map headers = new HashMap(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + for (Map.Entry header : headers.entrySet()) { + String name = header.getKey(); + String value = header.getValue(); + if (logger.isTraceEnabled()) { + logger.trace("Adding header '" + name + "' with value '"+value +"'"); + } + response.setHeader(header.getKey(), header.getValue()); + } + filterChain.doFilter(request, response); + } + + public void setHeaders(Map headers) { + this.headers.clear(); + this.headers.putAll(headers); + } + + public void addHeader(String name, String value) { + headers.put(name, value); + } +} diff --git a/web/src/test/java/org/springframework/security/web/headers/HeadersFilterTest.java b/web/src/test/java/org/springframework/security/web/headers/HeadersFilterTest.java new file mode 100644 index 0000000000..bb135eadf1 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/headers/HeadersFilterTest.java @@ -0,0 +1,73 @@ +/* + * 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.web.headers; + +import org.junit.Test; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.matchers.JUnitMatchers.hasItems; + +/** + * Tests for the {@code HeadersFilter} + * + * @author Marten Deinum + * @since 3.2 + */ +public class HeadersFilterTest { + + @Test + public void noHeadersConfigured() throws Exception { + HeadersFilter filter = new HeadersFilter(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + + filter.doFilter(request, response, filterChain); + + assertTrue(response.getHeaderNames().isEmpty()); + } + + @Test + public void additionalHeadersShouldBeAddedToTheResponse() throws Exception { + HeadersFilter filter = new HeadersFilter(); + Map headers = new HashMap(); + headers.put("X-Header1", "foo"); + headers.put("X-Header2", "bar"); + filter.setHeaders(headers); + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + + filter.doFilter(request, response, filterChain); + + Collection headerNames = response.getHeaderNames(); + assertThat(headerNames.size(), is(2)); + assertThat(headerNames, hasItems("X-Header1", "X-Header2")); + assertThat(response.getHeader("X-Header1"), is("foo")); + assertThat(response.getHeader("X-Header2"), is("bar")); + + } +}