Bladeren bron

SEC-1574: Add CSRF Support

Rob Winch 12 jaren geleden
bovenliggende
commit
e9bb9e766e
93 gewijzigde bestanden met toevoegingen van 2893 en 346 verwijderingen
  1. 1 0
      config/config.gradle
  2. 7 0
      config/pom.xml
  3. 1 0
      config/src/main/java/org/springframework/security/config/Elements.java
  4. 3 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java
  5. 12 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
  6. 40 0
      config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java
  7. 1 1
      config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java
  8. 39 0
      config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java
  9. 1 0
      config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java
  10. 11 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java
  11. 1 13
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java
  12. 121 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java
  13. 9 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java
  14. 18 6
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java
  15. 12 4
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java
  16. 9 1
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java
  17. 0 6
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java
  18. 18 3
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java
  19. 14 3
      config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java
  20. 88 0
      config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java
  21. 39 1
      config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
  22. 2 1
      config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java
  23. 7 4
      config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java
  24. 1 0
      config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
  25. 14 4
      config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc
  26. 29 1
      config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd
  27. 8 0
      config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy
  28. 23 2
      config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy
  29. 0 52
      config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy
  30. 2 3
      config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy
  31. 2 1
      config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy
  32. 355 0
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy
  33. 20 20
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy
  34. 12 23
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy
  35. 8 5
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy
  36. 4 1
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy
  37. 11 24
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy
  38. 22 10
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.groovy
  39. 6 16
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy
  40. 3 18
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy
  41. 0 15
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy
  42. 3 1
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy
  43. 9 5
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy
  44. 2 1
      config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy
  45. 5 2
      config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy
  46. 257 0
      config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy
  47. 1 3
      config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy
  48. 85 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerNoWebMvcTests.java
  49. 4 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java
  50. 8 0
      docs/guides/src/asciidoc/hello-includes/secure-the-application.asc
  51. 9 3
      docs/guides/src/asciidoc/hellomvc.asc
  52. 13 7
      docs/guides/src/asciidoc/helloworld.asc
  53. 25 0
      docs/manual/src/docbook/appendix-namespace.xml
  54. 6 1
      docs/manual/src/docbook/namespace-config.xml
  55. 6 3
      samples/concurrency-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  56. 2 3
      samples/contacts-xml/contacts.gradle
  57. 6 7
      samples/contacts-xml/pom.xml
  58. 1 0
      samples/contacts-xml/src/main/resources/applicationContext-security.xml
  59. 2 0
      samples/contacts-xml/src/main/webapp/WEB-INF/jsp/add.jsp
  60. 13 12
      samples/contacts-xml/src/main/webapp/WEB-INF/jsp/addPermission.jsp
  61. 1 0
      samples/contacts-xml/src/main/webapp/exitUser.jsp
  62. 1 1
      samples/contacts-xml/src/main/webapp/login.jsp
  63. 1 1
      samples/contacts-xml/src/main/webapp/switchUser.jsp
  64. 3 3
      samples/hellomvc-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  65. 5 4
      samples/helloworld-jc/src/main/webapp/index.jsp
  66. 6 3
      samples/inmemory-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  67. 6 1
      samples/insecuremvc/src/main/webapp/WEB-INF/decorators/main.jsp
  68. 6 3
      samples/jdbc-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  69. 6 3
      samples/ldap-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  70. 6 6
      samples/messages-jc/pom.xml
  71. 6 3
      samples/openid-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  72. 6 3
      samples/preauth-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  73. 6 3
      samples/rememberme-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  74. 6 3
      samples/x509-jc/src/main/webapp/WEB-INF/decorators/main.jsp
  75. 7 0
      web/pom.xml
  76. 47 22
      web/src/main/java/org/springframework/security/web/authentication/logout/LogoutFilter.java
  77. 1 1
      web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java
  78. 11 0
      web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java
  79. 56 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java
  80. 141 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java
  81. 54 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfLogoutHandler.java
  82. 78 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java
  83. 71 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRepository.java
  84. 109 0
      web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java
  85. 40 0
      web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java
  86. 86 0
      web/src/main/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessor.java
  87. 64 0
      web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java
  88. 303 0
      web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java
  89. 63 0
      web/src/test/java/org/springframework/security/web/csrf/CsrfLogoutHandlerTests.java
  90. 58 0
      web/src/test/java/org/springframework/security/web/csrf/CsrfTokenTests.java
  91. 127 0
      web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java
  92. 79 0
      web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java
  93. 2 0
      web/web.gradle

+ 1 - 0
config/config.gradle

@@ -19,6 +19,7 @@ dependencies {
              project(':spring-security-ldap'),
              project(':spring-security-openid'),
              "org.springframework:spring-web:$springVersion",
+             "org.springframework:spring-webmvc:$springVersion",
              "org.aspectj:aspectjweaver:$aspectjVersion"
 
     provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"

+ 7 - 0
config/pom.xml

@@ -133,6 +133,13 @@
       <scope>compile</scope>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-webmvc</artifactId>
+      <version>3.2.3.RELEASE</version>
+      <scope>compile</scope>
+      <optional>true</optional>
+    </dependency>
     <dependency>
       <groupId>org.apache.tomcat</groupId>
       <artifactId>tomcat-servlet-api</artifactId>

+ 1 - 0
config/src/main/java/org/springframework/security/config/Elements.java

@@ -55,4 +55,5 @@ public abstract class Elements {
     public static final String DEBUG = "debug";
     public static final String HTTP_FIREWALL = "http-firewall";
     public static final String HEADERS = "headers";
+    public static final String CSRF = "csrf";
 }

+ 3 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java

@@ -37,6 +37,7 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
 import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
 import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
+import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.security.web.header.HeaderWriterFilter;
 import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
 import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
@@ -69,6 +70,8 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
         order += STEP;
         put(HeaderWriterFilter.class, order);
         order += STEP;
+        put(CsrfFilter.class, order);
+        order += STEP;
         put(LogoutFilter.class, order);
         order += STEP;
         put(X509AuthenticationFilter.class, order);

+ 12 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -38,6 +38,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ChannelSecurityConfigurer;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
 import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
@@ -663,6 +664,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
         return getOrApply(new ServletApiConfigurer<HttpSecurity>());
     }
 
+
+    /**
+     * Adds CSRF support
+     *
+     * @return the {@link ServletApiConfigurer} for further customizations
+     * @throws Exception
+     */
+    public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
+        return getOrApply(new CsrfConfigurer<HttpSecurity>());
+    }
+
     /**
      * Provides logout support. This is automatically applied when using
      * {@link WebSecurityConfigurerAdapter}. The default is that accessing

+ 40 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configuration/CsrfWebMvcConfiguration.java

@@ -0,0 +1,40 @@
+/*
+ * 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.configuration;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.support.RequestDataValueProcessor;
+
+/**
+ * Used to add a {@link RequestDataValueProcessor} for Spring MVC and Spring
+ * Security CSRF integration. This configuration is added whenever
+ * {@link EnableWebMvc} is added by {@link SpringWebMvcImportSelector} and the
+ * DispatcherServlet is present on the classpath.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+@Configuration
+class CsrfWebMvcConfiguration {
+
+    @Bean
+    public RequestDataValueProcessor requestDataValueProcessor() {
+        return new CsrfRequestDataValueProcessor();
+    }
+}

+ 1 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configuration/EnableWebSecurity.java

@@ -77,7 +77,7 @@ import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
 @Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
 @Target(value={java.lang.annotation.ElementType.TYPE})
 @Documented
-@Import({WebSecurityConfiguration.class,ObjectPostProcessorConfiguration.class,AuthenticationConfiguration.class})
+@Import({WebSecurityConfiguration.class,ObjectPostProcessorConfiguration.class,AuthenticationConfiguration.class, SpringWebMvcImportSelector.class})
 public @interface EnableWebSecurity {
 
     /**

+ 39 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configuration/SpringWebMvcImportSelector.java

@@ -0,0 +1,39 @@
+/*
+ * 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.configuration;
+
+import org.springframework.context.annotation.ImportSelector;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Used by {@link EnableWebSecurity} to conditionaly import
+ * {@link CsrfWebMvcConfiguration} when the DispatcherServlet is present on the
+ * classpath.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+class SpringWebMvcImportSelector implements ImportSelector {
+
+    /* (non-Javadoc)
+     * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata)
+     */
+    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
+        boolean webmvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", getClass().getClassLoader());
+        return webmvcPresent ? new String[] {"org.springframework.security.config.annotation.web.configuration.CsrfWebMvcConfiguration"} : new String[] {};
+    }
+}

+ 1 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java

@@ -154,6 +154,7 @@ public abstract class WebSecurityConfigurerAdapter implements SecurityConfigurer
         http.setSharedObject(ContentNegotiationStrategy.class, contentNegotiationStrategy);
         if(!disableDefaults) {
             http
+                .csrf().and()
                 .addFilter(new WebAsyncManagerIntegrationFilter())
                 .exceptionHandling().and()
                 .headers().and()

+ 11 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractHttpConfigurer.java

@@ -30,4 +30,15 @@ import org.springframework.security.web.DefaultSecurityFilterChain;
  */
 abstract class AbstractHttpConfigurer<B extends HttpSecurityBuilder<B>> extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, B> {
 
+    /**
+     * Disables the {@link AbstractHttpConfigurer} by removing it. After doing
+     * so a fresh version of the configuration can be applied.
+     *
+     * @return the {@link HttpSecurityBuilder} for additional customizations
+     */
+    @SuppressWarnings("unchecked")
+    public B disable() {
+        getBuilder().removeConfigurer(getClass());
+        return getBuilder();
+    }
 }

+ 1 - 13
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurer.java

@@ -39,7 +39,7 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
  * @author  Rob Winch
  * @since  3.2
  */
-public final class AnonymousConfigurer<H extends HttpSecurityBuilder<H>> extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,H> {
+public final class AnonymousConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<H> {
     private String key;
     private AuthenticationProvider authenticationProvider;
     private AnonymousAuthenticationFilter authenticationFilter;
@@ -53,18 +53,6 @@ public final class AnonymousConfigurer<H extends HttpSecurityBuilder<H>> extends
     public AnonymousConfigurer() {
     }
 
-    /**
-     * Disables anonymous authentication.
-     *
-     * @return the {@link HttpSecurity} since no further customization of anonymous authentication would be
-     *         meaningful.
-     */
-    @SuppressWarnings("unchecked")
-    public H disable() {
-        getBuilder().removeConfigurer(getClass());
-        return getBuilder();
-    }
-
     /**
      * Sets the key to identify tokens created for anonymous authentication. Default is a secure randomly generated
      * key.

+ 121 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

@@ -0,0 +1,121 @@
+/*
+ * 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.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.csrf.CsrfLogoutHandler;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
+import org.springframework.security.web.util.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Adds <a
+ * href="https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)"
+ * >CSRF</a> protection for the methods as specified by
+ * {@link #requireCsrfProtectionMatcher(RequestMatcher)}.
+ *
+ * <h2>Security Filters</h2>
+ *
+ * The following Filters are populated
+ *
+ * <ul>
+ * <li>{@link CsrfFilter}</li>
+ * </ul>
+ *
+ * <h2>Shared Objects Created</h2>
+ *
+ * No shared objects are created.
+ *
+ * <h2>Shared Objects Used</h2>
+ *
+ * <ul>
+ * <li>
+ * {@link ExceptionHandlingConfigurer#accessDeniedHandler(AccessDeniedHandler)}
+ * is used to determine how to handle CSRF attempts</li>
+ * </ul>
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<H> {
+    private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
+    private RequestMatcher requireCsrfProtectionMatcher;
+
+    /**
+     * Creates a new instance
+     * @see HttpSecurity#csrf()
+     */
+    public CsrfConfigurer() {
+    }
+
+    /**
+     * Specify the {@link CsrfTokenRepository} to use. The default is an {@link HttpSessionCsrfTokenRepository}.
+     *
+     * @param csrfTokenRepository the {@link CsrfTokenRepository} to use
+     * @return the {@link CsrfConfigurer} for further customizations
+     */
+    public CsrfConfigurer<H> csrfTokenRepository(CsrfTokenRepository csrfTokenRepository) {
+        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
+        this.csrfTokenRepository = csrfTokenRepository;
+        return this;
+    }
+
+    /**
+     * Specify the {@link RequestMatcher} to use for determining when CSRF
+     * should be applied. The default is to ignore GET, HEAD, TRACE, OPTIONS and
+     * process all other requests.
+     *
+     * @param requireCsrfProtectionMatcher
+     *            the {@link RequestMatcher} to use
+     * @return the {@link CsrfConfigurer} for further customizations
+     */
+    public CsrfConfigurer<H> requireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
+        Assert.notNull(csrfTokenRepository, "requireCsrfProtectionMatcher cannot be null");
+        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void configure(H http) throws Exception {
+        CsrfFilter filter = new CsrfFilter(csrfTokenRepository);
+        if(requireCsrfProtectionMatcher != null) {
+            filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
+        }
+        ExceptionHandlingConfigurer<H> exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class);
+        if(exceptionConfig != null) {
+            AccessDeniedHandler accessDeniedHandler = exceptionConfig.getAccessDeniedHandler();
+            if(accessDeniedHandler != null) {
+                filter.setAccessDeniedHandler(accessDeniedHandler);
+            }
+        }
+        LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
+        if(logoutConfigurer != null) {
+            logoutConfigurer.addLogoutHandler(new CsrfLogoutHandler(csrfTokenRepository));
+        }
+        SessionManagementConfigurer<H> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
+        if(sessionConfigurer != null) {
+            sessionConfigurer.addSessionAuthenticationStrategy(new CsrfAuthenticationStrategy(csrfTokenRepository));
+        }
+        filter = postProcess(filter);
+        http.addFilter(filter);
+    }
+}

+ 9 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java

@@ -153,6 +153,15 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
         return this.authenticationEntryPoint;
     }
 
+    /**
+     * Gets the {@link AccessDeniedHandler} that is configured.
+     *
+     * @return the {@link AccessDeniedHandler}
+     */
+    AccessDeniedHandler getAccessDeniedHandler() {
+        return this.accessDeniedHandler;
+    }
+
     @Override
     public void configure(H http) throws Exception {
         AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);

+ 18 - 6
config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java

@@ -16,7 +16,6 @@
 package org.springframework.security.config.annotation.web.configurers;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 import javax.servlet.http.HttpSession;
@@ -31,6 +30,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
 import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
 import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFilter;
+import org.springframework.security.web.util.AntPathRequestMatcher;
+import org.springframework.security.web.util.RequestMatcher;
 
 /**
  * Adds logout support. Other {@link SecurityConfigurer} instances may invoke
@@ -63,7 +64,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends Ab
     private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
     private String logoutSuccessUrl = "/login?logout";
     private LogoutSuccessHandler logoutSuccessHandler;
-    private String logoutUrl = "/logout";
+    private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/logout", "POST");
     private boolean permitAll;
     private boolean customLogoutSuccess;
 
@@ -97,12 +98,22 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends Ab
     }
 
     /**
-     * The URL that triggers logout to occur. The default is "/logout"
+     * The URL that triggers logout to occur on HTTP POST. The default is "/logout"
      * @param logoutUrl the URL that will invoke logout.
      * @return the {@link LogoutConfigurer} for further customization
      */
     public LogoutConfigurer<H> logoutUrl(String logoutUrl) {
-        this.logoutUrl = logoutUrl;
+        return logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "POST"));
+    }
+
+
+    /**
+     * The RequestMatcher that triggers logout to occur on HTTP POST. The default is "/logout"
+     * @param logoutRequestMatcher the RequestMatcher used to determine if logout should occur.
+     * @return the {@link LogoutConfigurer} for further customization
+     */
+    public LogoutConfigurer<H> logoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
+        this.logoutRequestMatcher = logoutRequestMatcher;
         return this;
     }
 
@@ -189,7 +200,8 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends Ab
     @Override
     public void init(H http) throws Exception {
         if(permitAll) {
-            PermitAllSupport.permitAll(http, this.logoutUrl, this.logoutSuccessUrl);
+            PermitAllSupport.permitAll(http, this.logoutSuccessUrl);
+            PermitAllSupport.permitAll(http, this.logoutRequestMatcher);
         }
 
         DefaultLoginPageViewFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageViewFilter.class);
@@ -245,7 +257,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends Ab
         logoutHandlers.add(contextLogoutHandler);
         LogoutHandler[] handlers = logoutHandlers.toArray(new LogoutHandler[logoutHandlers.size()]);
         LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
-        result.setFilterProcessesUrl(logoutUrl);
+        result.setLogoutRequestMatcher(logoutRequestMatcher);
         result = postProcess(result);
         return result;
     }

+ 12 - 4
config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java

@@ -31,17 +31,25 @@ import org.springframework.security.web.util.RequestMatcher;
  */
 final class PermitAllSupport {
 
-    @SuppressWarnings("unchecked")
     public static void permitAll(HttpSecurityBuilder<? extends HttpSecurityBuilder<?>> http, String... urls) {
+        for(String url : urls) {
+            if(url != null) {
+                permitAll(http, new ExactUrlRequestMatcher(url));
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public static void permitAll(HttpSecurityBuilder<? extends HttpSecurityBuilder<?>> http, RequestMatcher... requestMatchers) {
         ExpressionUrlAuthorizationConfigurer<?> configurer = http.getConfigurer(ExpressionUrlAuthorizationConfigurer.class);
 
         if(configurer == null) {
             throw new IllegalStateException("permitAll only works with HttpSecurity.authorizeRequests()");
         }
 
-        for(String url : urls) {
-            if(url != null) {
-                configurer.addMapping(0, new UrlMapping(new ExactUrlRequestMatcher(url), SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll)));
+        for(RequestMatcher matcher : requestMatchers) {
+            if(matcher != null) {
+                configurer.addMapping(0, new UrlMapping(matcher, SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll)));
             }
         }
     }

+ 9 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java

@@ -20,6 +20,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
 import org.springframework.security.web.savedrequest.RequestCache;
 import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
+import org.springframework.security.web.util.AntPathRequestMatcher;
 
 /**
  * Adds request cache for Spring Security. Specifically this ensures that
@@ -71,6 +72,11 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
         return this;
     }
 
+    @Override
+    public void init(H http) throws Exception {
+        http.setSharedObject(RequestCache.class, getRequestCache(http));
+    }
+
     @Override
     public void configure(H http) throws Exception {
         RequestCache requestCache = getRequestCache(http);
@@ -93,6 +99,8 @@ public final class RequestCacheConfigurer<H extends HttpSecurityBuilder<H>> exte
         if(result != null) {
             return result;
         }
-        return new HttpSessionRequestCache();
+        HttpSessionRequestCache defaultCache = new HttpSessionRequestCache();
+        defaultCache.setRequestMatcher(new AntPathRequestMatcher("/**", "GET"));
+        return defaultCache;
     }
 }

+ 0 - 6
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java

@@ -64,12 +64,6 @@ public final class ServletApiConfigurer<H extends HttpSecurityBuilder<H>> extend
         return this;
     }
 
-    @SuppressWarnings("unchecked")
-    public H disable() {
-        getBuilder().removeConfigurer(getClass());
-        return getBuilder();
-    }
-
     @Override
     @SuppressWarnings("unchecked")
     public void configure(H http) throws Exception {

+ 18 - 3
config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java

@@ -15,6 +15,7 @@
  */
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -80,6 +81,7 @@ import org.springframework.util.Assert;
 public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<H> {
     private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = createDefaultSessionFixationProtectionStrategy();
     private SessionAuthenticationStrategy sessionAuthenticationStrategy;
+    private List<SessionAuthenticationStrategy> sessionAuthenticationStrategies = new ArrayList<SessionAuthenticationStrategy>();
     private SessionRegistry sessionRegistry = new SessionRegistryImpl();
     private Integer maximumSessions;
     private String expiredUrl;
@@ -173,6 +175,18 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
         return this;
     }
 
+    /**
+     * Adds an additional {@link SessionAuthenticationStrategy} to be used within the {@link CompositeSessionAuthenticationStrategy}.
+     *
+     * @param sessionAuthenticationStrategy
+     * @return the {@link SessionManagementConfigurer} for further
+     *         customizations
+     */
+    SessionManagementConfigurer<H> addSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
+        this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy);
+        return this;
+    }
+
     public SessionFixationConfigurer sessionFixation() {
         return new SessionFixationConfigurer();
     }
@@ -400,6 +414,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
         if(sessionAuthenticationStrategy != null) {
             return sessionAuthenticationStrategy;
         }
+        List<SessionAuthenticationStrategy> delegateStrategies = sessionAuthenticationStrategies;
         if(isConcurrentSessionControlEnabled()) {
             ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
             concurrentSessionControlStrategy.setMaximumSessions(maximumSessions);
@@ -409,11 +424,11 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
             RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(sessionRegistry);
             registerSessionStrategy = postProcess(registerSessionStrategy);
 
-            List<SessionAuthenticationStrategy> delegateStrategies = Arrays.asList(concurrentSessionControlStrategy, sessionFixationAuthenticationStrategy, registerSessionStrategy);
-            sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(delegateStrategies));
+            delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy, sessionFixationAuthenticationStrategy, registerSessionStrategy));
         } else {
-            sessionAuthenticationStrategy = sessionFixationAuthenticationStrategy;
+            delegateStrategies.add(sessionFixationAuthenticationStrategy);
         }
+        sessionAuthenticationStrategy = postProcess(new CompositeSessionAuthenticationStrategy(delegateStrategies));
         return sessionAuthenticationStrategy;
     }
 

+ 14 - 3
config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java

@@ -116,6 +116,7 @@ final class AuthenticationConfigBuilder {
     private BeanReference jeeProviderRef;
     private RootBeanDefinition preAuthEntryPoint;
     private BeanMetadataElement mainEntryPoint;
+    private BeanMetadataElement accessDeniedHandler;
 
     private BeanDefinition logoutFilter;
     @SuppressWarnings("rawtypes")
@@ -125,9 +126,10 @@ final class AuthenticationConfigBuilder {
     private final BeanReference requestCache;
     private final BeanReference portMapper;
     private final BeanReference portResolver;
+    private final BeanMetadataElement csrfLogoutHandler;
 
     public AuthenticationConfigBuilder(Element element, ParserContext pc, SessionCreationPolicy sessionPolicy,
-            BeanReference requestCache, BeanReference authenticationManager, BeanReference sessionStrategy, BeanReference portMapper, BeanReference portResolver) {
+            BeanReference requestCache, BeanReference authenticationManager, BeanReference sessionStrategy, BeanReference portMapper, BeanReference portResolver, BeanMetadataElement csrfLogoutHandler) {
         this.httpElt = element;
         this.pc = pc;
         this.requestCache = requestCache;
@@ -136,6 +138,7 @@ final class AuthenticationConfigBuilder {
                 && sessionPolicy != SessionCreationPolicy.STATELESS;
         this.portMapper = portMapper;
         this.portResolver = portResolver;
+        this.csrfLogoutHandler = csrfLogoutHandler;
 
         createAnonymousFilter();
         createRememberMeFilter(authenticationManager);
@@ -483,7 +486,7 @@ final class AuthenticationConfigBuilder {
     void createLogoutFilter() {
         Element logoutElt = DomUtils.getChildElementByTagName(httpElt, Elements.LOGOUT);
         if (logoutElt != null || autoConfig) {
-            LogoutBeanDefinitionParser logoutParser = new LogoutBeanDefinitionParser(rememberMeServicesId);
+            LogoutBeanDefinitionParser logoutParser = new LogoutBeanDefinitionParser(rememberMeServicesId, csrfLogoutHandler);
             logoutFilter = logoutParser.parse(logoutElt, pc);
             logoutHandlers = logoutParser.getLogoutHandlers();
         }
@@ -493,6 +496,9 @@ final class AuthenticationConfigBuilder {
     ManagedList getLogoutHandlers() {
         if(logoutHandlers == null && rememberMeProviderRef != null) {
             logoutHandlers = new ManagedList();
+            if(csrfLogoutHandler != null) {
+                logoutHandlers.add(csrfLogoutHandler);
+            }
             logoutHandlers.add(new RuntimeBeanReference(rememberMeServicesId));
             logoutHandlers.add(new RootBeanDefinition(SecurityContextLogoutHandler.class));
         }
@@ -504,6 +510,10 @@ final class AuthenticationConfigBuilder {
         return mainEntryPoint;
     }
 
+    BeanMetadataElement getAccessDeniedHandlerBean() {
+        return accessDeniedHandler;
+    }
+
     void createAnonymousFilter() {
         Element anonymousElt = DomUtils.getChildElementByTagName(httpElt, Elements.ANONYMOUS);
 
@@ -559,7 +569,8 @@ final class AuthenticationConfigBuilder {
 
     void createExceptionTranslationFilter() {
         BeanDefinitionBuilder etfBuilder = BeanDefinitionBuilder.rootBeanDefinition(ExceptionTranslationFilter.class);
-        etfBuilder.addPropertyValue("accessDeniedHandler", createAccessDeniedHandler(httpElt, pc));
+        accessDeniedHandler = createAccessDeniedHandler(httpElt, pc);
+        etfBuilder.addPropertyValue("accessDeniedHandler", accessDeniedHandler);
         assert requestCache != null;
         mainEntryPoint = selectEntryPoint();
         etfBuilder.addConstructorArgValue(mainEntryPoint);

+ 88 - 0
config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java

@@ -0,0 +1,88 @@
+/*
+ * 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.parsing.BeanComponentDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.csrf.CsrfLogoutHandler;
+import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
+import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+import org.w3c.dom.Element;
+
+/**
+ * Parser for the {@code CsrfFilter}.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
+
+    private static final String REQUEST_DATA_VALUE_PROCESSOR = "requestDataValueProcessor";
+    private static final String DISPATCHER_SERVLET_CLASS_NAME = "org.springframework.web.servlet.DispatcherServlet";
+    private static final String ATT_MATCHER = "request-matcher-ref";
+    private static final String ATT_REPOSITORY = "token-repository-ref";
+
+    private String csrfRepositoryRef;
+
+    public BeanDefinition parse(Element element, ParserContext pc) {
+        boolean webmvcPresent = ClassUtils.isPresent(DISPATCHER_SERVLET_CLASS_NAME, getClass().getClassLoader());
+        if(webmvcPresent) {
+            RootBeanDefinition beanDefinition = new RootBeanDefinition(CsrfRequestDataValueProcessor.class);
+            BeanComponentDefinition componentDefinition =
+                    new BeanComponentDefinition(beanDefinition, REQUEST_DATA_VALUE_PROCESSOR);
+            pc.registerBeanComponent(componentDefinition);
+        }
+
+        csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY);
+        String matcherRef = element.getAttribute(ATT_MATCHER);
+
+        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(CsrfFilter.class);
+
+        if(!StringUtils.hasText(csrfRepositoryRef)) {
+            RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class);
+            csrfRepositoryRef = pc.getReaderContext().generateBeanName(csrfTokenRepository);
+            pc.registerBeanComponent(new BeanComponentDefinition(csrfTokenRepository, csrfRepositoryRef));
+        }
+
+        builder.addConstructorArgReference(csrfRepositoryRef);
+
+        if(StringUtils.hasText(matcherRef)) {
+            builder.addPropertyReference("requireCsrfProtectionMatcher", matcherRef);
+        }
+
+        return builder.getBeanDefinition();
+    }
+
+    BeanDefinition getCsrfAuthenticationStrategy() {
+        BeanDefinitionBuilder csrfAuthenticationStrategy = BeanDefinitionBuilder.rootBeanDefinition(CsrfAuthenticationStrategy.class);
+        csrfAuthenticationStrategy.addConstructorArgReference(csrfRepositoryRef);
+        return csrfAuthenticationStrategy.getBeanDefinition();
+    }
+
+    BeanDefinition getCsrfLogoutHandler() {
+        BeanDefinitionBuilder csrfAuthenticationStrategy = BeanDefinitionBuilder.rootBeanDefinition(CsrfLogoutHandler.class);
+        csrfAuthenticationStrategy.addConstructorArgReference(csrfRepositoryRef);
+        return csrfAuthenticationStrategy.getBeanDefinition();
+    }
+}

+ 39 - 1
config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

@@ -28,7 +28,6 @@ import javax.servlet.http.HttpServletRequest;
 import org.springframework.beans.BeanMetadataElement;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanReference;
-import org.springframework.beans.factory.config.BeanReferenceFactoryBean;
 import org.springframework.beans.factory.config.RuntimeBeanReference;
 import org.springframework.beans.factory.parsing.BeanComponentDefinition;
 import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
@@ -71,6 +70,7 @@ import org.springframework.security.web.servletapi.SecurityContextHolderAwareReq
 import org.springframework.security.web.session.ConcurrentSessionFilter;
 import org.springframework.security.web.session.SessionManagementFilter;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
+import org.springframework.security.web.util.AntPathRequestMatcher;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.ReflectionUtils;
 import org.springframework.util.StringUtils;
@@ -126,6 +126,9 @@ class HttpConfigurationBuilder {
     private BeanReference fsi;
     private BeanReference requestCache;
     private BeanDefinition addHeadersFilter;
+    private BeanDefinition csrfFilter;
+    private BeanMetadataElement csrfLogoutHandler;
+    private BeanMetadataElement csrfAuthStrategy;
 
     public HttpConfigurationBuilder(Element element, ParserContext pc,
             BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) {
@@ -152,6 +155,7 @@ class HttpConfigurationBuilder {
             sessionPolicy = SessionCreationPolicy.IF_REQUIRED;
         }
 
+        createCsrfFilter();
         createSecurityContextPersistenceFilter();
         createSessionManagementFilters();
         createWebAsyncManagerFilter();
@@ -195,6 +199,12 @@ class HttpConfigurationBuilder {
         }
     }
 
+    void setAccessDeniedHandler(BeanMetadataElement accessDeniedHandler) {
+        if(csrfFilter != null) {
+            csrfFilter.getPropertyValues().add("accessDeniedHandler", accessDeniedHandler);
+        }
+    }
+
     // Needed to account for placeholders
     static String createPath(String path, boolean lowerCase) {
         return lowerCase ? path.toLowerCase() : path;
@@ -298,6 +308,10 @@ class HttpConfigurationBuilder {
         BeanDefinitionBuilder sessionFixationStrategy = null;
         BeanDefinitionBuilder registerSessionStrategy;
 
+        if(csrfAuthStrategy != null) {
+            delegateSessionStrategies.add(csrfAuthStrategy);
+        }
+
         if (sessionControlEnabled) {
             assert sessionRegistryRef != null;
             concurrentSessionStrategy = BeanDefinitionBuilder.rootBeanDefinition(ConcurrentSessionControlAuthenticationStrategy.class);
@@ -541,6 +555,12 @@ class HttpConfigurationBuilder {
                 requestCacheBldr = BeanDefinitionBuilder.rootBeanDefinition(HttpSessionRequestCache.class);
                 requestCacheBldr.addPropertyValue("createSessionAllowed", sessionPolicy == SessionCreationPolicy.IF_REQUIRED);
                 requestCacheBldr.addPropertyValue("portResolver", portResolver);
+                if(csrfFilter != null) {
+                    BeanDefinitionBuilder requestCacheMatcherBldr = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class);
+                    requestCacheMatcherBldr.addConstructorArgValue("/**");
+                    requestCacheMatcherBldr.addConstructorArgValue("GET");
+                    requestCacheBldr.addPropertyValue("requestMatcher", requestCacheMatcherBldr.getBeanDefinition());
+                }
             }
 
             BeanDefinition bean = requestCacheBldr.getBeanDefinition();
@@ -617,6 +637,20 @@ class HttpConfigurationBuilder {
 
     }
 
+    private void createCsrfFilter() {
+        Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.CSRF);
+        if (elmt != null) {
+            CsrfBeanDefinitionParser csrfParser = new CsrfBeanDefinitionParser();
+            this.csrfFilter = csrfParser.parse(elmt, pc);
+            this.csrfAuthStrategy = csrfParser.getCsrfAuthenticationStrategy();
+            this.csrfLogoutHandler = csrfParser.getCsrfLogoutHandler();
+        }
+    }
+
+    BeanMetadataElement getCsrfLogoutHandler() {
+        return this.csrfLogoutHandler;
+    }
+
     BeanReference getSessionStrategy() {
         return sessionStrategyRef;
     }
@@ -668,6 +702,10 @@ class HttpConfigurationBuilder {
             filters.add(new OrderDecorator(addHeadersFilter, HEADERS_FILTER));
         }
 
+        if (csrfFilter != null) {
+            filters.add(new OrderDecorator(csrfFilter, CSRF_FILTER));
+        }
+
         return filters;
     }
 }

+ 2 - 1
config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java

@@ -137,10 +137,11 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
 
         AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, pc,
                 httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager,
-                httpBldr.getSessionStrategy(), portMapper, portResolver);
+                httpBldr.getSessionStrategy(), portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
 
         httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers());
         httpBldr.setEntryPoint(authBldr.getEntryPointBean());
+        httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean());
 
         authenticationProviders.addAll(authBldr.getProviders());
 

+ 7 - 4
config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java

@@ -15,6 +15,7 @@
  */
 package org.springframework.security.config.http;
 
+import org.springframework.beans.BeanMetadataElement;
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.RuntimeBeanReference;
 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -44,13 +45,15 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
     static final String ATT_DELETE_COOKIES = "delete-cookies";
 
     final String rememberMeServices;
-    private ManagedList logoutHandlers = new ManagedList();
+    private ManagedList<BeanMetadataElement> logoutHandlers = new ManagedList<BeanMetadataElement>();
 
-    public LogoutBeanDefinitionParser(String rememberMeServices) {
+    public LogoutBeanDefinitionParser(String rememberMeServices, BeanMetadataElement csrfLogoutHandler) {
         this.rememberMeServices = rememberMeServices;
+        if(csrfLogoutHandler != null) {
+            logoutHandlers.add(csrfLogoutHandler);
+        }
     }
 
-    @SuppressWarnings("unchecked")
     public BeanDefinition parse(Element element, ParserContext pc) {
         String logoutUrl = null;
         String successHandlerRef = null;
@@ -111,7 +114,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
         return builder.getBeanDefinition();
     }
 
-    ManagedList getLogoutHandlers() {
+    ManagedList<BeanMetadataElement> getLogoutHandlers() {
         return logoutHandlers;
     }
 }

+ 1 - 0
config/src/main/java/org/springframework/security/config/http/SecurityFilters.java

@@ -30,6 +30,7 @@ enum SecurityFilters {
     /** {@link WebAsyncManagerIntegrationFilter} */
     WEB_ASYNC_MANAGER_FILTER,
     HEADERS_FILTER,
+    CSRF_FILTER,
     LOGOUT_FILTER,
     X509_FILTER,
     PRE_AUTH_FILTER,

+ 14 - 4
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? & headers?) }
+    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? & csrf?) }
 http.attlist &=
     ## The request URL pattern which will be mapped to the filter chain created by this <http> element. If omitted, the filter chain will match all requests.
     attribute pattern {xsd:token}?
@@ -718,8 +718,18 @@ jdbc-user-service.attlist &=
 jdbc-user-service.attlist &=
     role-prefix?
 
+csrf =
+ ## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests.
+    element csrf {csrf-options.attlist}
+csrf-options.attlist &=
+    ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
+    attribute request-matcher-ref { xsd:token }?
+csrf-options.attlist &=
+    ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
+    attribute token-repository-ref { xsd:token }?
+
 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 for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.
  element headers {cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & header*}
 
 hsts =
@@ -783,7 +793,7 @@ header.attlist &=
     ## The value for the header.
     attribute value {xsd:token}?
 header.attlist &=
-    ## Reference to a custom HeaderFactory implementation.
+    ## Reference to a custom HeaderWriter implementation.
     ref?
 
 any-user-service = user-service | jdbc-user-service | ldap-user-service
@@ -808,4 +818,4 @@ position =
     ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter.
     attribute position {named-security-filter}
 
-named-security-filter = "FIRST" | "CHANNEL_FILTER" | "CONCURRENT_SESSION_FILTER" | "SECURITY_CONTEXT_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" |"BASIC_AUTH_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "SESSION_MANAGEMENT_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST"
+named-security-filter = "FIRST" | "CHANNEL_FILTER" | "CONCURRENT_SESSION_FILTER" | "HEADERS_FILTER" | "CSRF_FILTER" | "SECURITY_CONTEXT_FILTER" | "LOGOUT_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" |"BASIC_AUTH_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "SESSION_MANAGEMENT_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST"

+ 29 - 1
config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd

@@ -1025,6 +1025,7 @@
                </xs:complexType>
             </xs:element>
             <xs:element ref="security:headers"/>
+            <xs:element ref="security:csrf"/>
          </xs:choice>
          <xs:attributeGroup ref="security:http.attlist"/>
       </xs:complexType>
@@ -2238,9 +2239,34 @@
          </xs:annotation>
       </xs:attribute>
   </xs:attributeGroup>
+  <xs:element name="csrf">
+      <xs:annotation>
+         <xs:documentation>Element for configuration of the CsrfFilter for protection against CSRF. It also updates
+                the default RequestCache to only replay "GET" requests.
+                </xs:documentation>
+      </xs:annotation>
+      <xs:complexType>
+         <xs:attributeGroup ref="security:csrf-options.attlist"/>
+      </xs:complexType>
+   </xs:element>
+  <xs:attributeGroup name="csrf-options.attlist">
+      <xs:attribute name="request-matcher-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The RequestMatcher instance to be used to determine if CSRF should be applied. Default is
+                any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="token-repository-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+  </xs:attributeGroup>
   <xs:element name="headers">
       <xs:annotation>
-         <xs:documentation>Element for configuration of the AddHeadersFilter. Enables easy setting for the
+         <xs:documentation>Element for configuration of the HeaderWritersFilter. Enables easy setting for the
                 X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.
                 </xs:documentation>
       </xs:annotation>
@@ -2479,6 +2505,8 @@
          <xs:enumeration value="FIRST"/>
          <xs:enumeration value="CHANNEL_FILTER"/>
          <xs:enumeration value="CONCURRENT_SESSION_FILTER"/>
+         <xs:enumeration value="HEADERS_FILTER"/>
+         <xs:enumeration value="CSRF_FILTER"/>
          <xs:enumeration value="SECURITY_CONTEXT_FILTER"/>
          <xs:enumeration value="LOGOUT_FILTER"/>
          <xs:enumeration value="X509_FILTER"/>

+ 8 - 0
config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy

@@ -1,6 +1,8 @@
 package org.springframework.security.config
 
 import groovy.xml.MarkupBuilder
+
+import org.mockito.Mockito;
 import org.springframework.context.support.AbstractXmlApplicationContext
 import org.springframework.security.config.util.InMemoryXmlApplicationContext
 import org.springframework.security.core.context.SecurityContextHolder
@@ -37,6 +39,12 @@ abstract class AbstractXmlConfigTests extends Specification {
         SecurityContextHolder.clearContext();
     }
 
+    def mockBean(Class clazz, String id = clazz.simpleName) {
+        xml.'b:bean'(id: id, 'class': Mockito.class.name, 'factory-method':'mock') {
+            'b:constructor-arg'(value : clazz.name)
+        }
+    }
+
     def bean(String name, Class clazz) {
         xml.'b:bean'(id: name, 'class': clazz.name)
     }

+ 23 - 2
config/src/test/groovy/org/springframework/security/config/annotation/BaseSpringSpec.groovy

@@ -25,16 +25,18 @@ import org.springframework.mock.web.MockHttpServletRequest
 import org.springframework.mock.web.MockHttpServletResponse
 import org.springframework.security.authentication.AuthenticationManager
 import org.springframework.security.authentication.AuthenticationProvider
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
 import org.springframework.security.core.Authentication
-import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.authority.AuthorityUtils
 import org.springframework.security.core.context.SecurityContextHolder
 import org.springframework.security.core.context.SecurityContextImpl
 import org.springframework.security.web.FilterChainProxy
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
 import org.springframework.security.web.context.HttpRequestResponseHolder
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository
+import org.springframework.security.web.csrf.CsrfToken
+import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository
 
 import spock.lang.AutoCleanup
 import spock.lang.Specification
@@ -50,11 +52,26 @@ abstract class BaseSpringSpec extends Specification {
     MockHttpServletRequest request
     MockHttpServletResponse response
     MockFilterChain chain
+    CsrfToken csrfToken
 
     def setup() {
+        setupWeb(null)
+    }
+
+    def setupWeb(httpSession = null) {
         request = new MockHttpServletRequest(method:"GET")
+        if(httpSession) {
+            request.session = httpSession
+        }
         response = new MockHttpServletResponse()
         chain = new MockFilterChain()
+        setupCsrf()
+    }
+
+    def setupCsrf(csrfTokenValue="BaseSpringSpec_CSRFTOKEN") {
+        csrfToken = new CsrfToken("X-CSRF-TOKEN","_csrf",csrfTokenValue)
+        new HttpSessionCsrfTokenRepository().saveToken(csrfToken, request,response)
+        request.setParameter(csrfToken.parameterName, csrfToken.token)
     }
 
     AuthenticationManagerBuilder authenticationBldr = new AuthenticationManagerBuilder(ObjectPostProcessor.QUIESCENT_POSTPROCESSOR).inMemoryAuthentication().and()
@@ -117,6 +134,10 @@ abstract class BaseSpringSpec extends Specification {
         authenticationProviders().find { provider.isAssignableFrom(it.class) }
     }
 
+    def getCurrentAuthentication() {
+        new HttpSessionSecurityContextRepository().loadContext(new HttpRequestResponseHolder(request, response)).authentication
+    }
+
     def login(String username="user", String role="ROLE_USER") {
         login(new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.createAuthorityList(role)))
     }

+ 0 - 52
config/src/test/groovy/org/springframework/security/config/annotation/BaseWebSpecuritySpec.groovy

@@ -1,52 +0,0 @@
-/*
- * 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;
-
-import org.springframework.context.ConfigurableApplicationContext
-import org.springframework.context.annotation.AnnotationConfigApplicationContext;
-import org.springframework.mock.web.MockFilterChain;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.security.core.context.SecurityContextHolder
-import org.springframework.security.web.FilterChainProxy;
-
-import spock.lang.AutoCleanup
-import spock.lang.Specification
-
-/**
- *
- * @author Rob Winch
- */
-abstract class BaseWebSpecuritySpec extends BaseSpringSpec {
-    FilterChainProxy springSecurityFilterChain
-    MockHttpServletRequest request
-    MockHttpServletResponse response
-    MockFilterChain chain
-
-    def setup() {
-        request = new MockHttpServletRequest(method:"GET")
-        response = new MockHttpServletResponse()
-        chain = new MockFilterChain()
-    }
-
-
-    def loadConfig(Class<?>... configs) {
-        super.loadConfig(configs)
-        springSecurityFilterChain = context.getBean(FilterChainProxy)
-    }
-
-
-}

+ 2 - 3
config/src/test/groovy/org/springframework/security/config/annotation/web/SampleWebSecurityConfigurerAdapterTests.groovy

@@ -20,8 +20,7 @@ import javax.servlet.http.HttpServletResponse
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.context.annotation.Configuration
 import org.springframework.core.annotation.Order
-import org.springframework.security.authentication.AuthenticationManager
-import org.springframework.security.config.annotation.BaseWebSpecuritySpec
+import org.springframework.security.config.annotation.BaseSpringSpec
 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
@@ -34,7 +33,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
  * @author Rob Winch
  *
  */
-public class SampleWebSecurityConfigurerAdapterTests extends BaseWebSpecuritySpec {
+public class SampleWebSecurityConfigurerAdapterTests extends BaseSpringSpec {
     def "README HelloWorld Sample works"() {
         setup: "Sample Config is loaded"
             loadConfig(HelloWorldWebSecurityConfigurerAdapter)

+ 2 - 1
config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy

@@ -79,7 +79,8 @@ class WebSecurityConfigurerAdapterTests extends BaseSpringSpec {
                          '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']
+                         'X-XSS-Protection' : '1; mode=block',
+                         'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @EnableWebSecurity

+ 355 - 0
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.groovy

@@ -0,0 +1,355 @@
+/*
+ * 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 javax.servlet.http.HttpServletResponse
+
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.BaseSpringSpec
+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.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
+import org.springframework.security.web.access.AccessDeniedHandler
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
+import org.springframework.security.web.util.RequestMatcher;
+
+import spock.lang.Unroll;
+
+/**
+ *
+ * @author Rob Winch
+ */
+class CsrfConfigurerTests extends BaseSpringSpec {
+
+    @Unroll
+    def "csrf applied by default"() {
+        setup:
+            loadConfig(CsrfAppliedDefaultConfig)
+            request.method = httpMethod
+            clearCsrfToken()
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == httpStatus
+        where:
+            httpMethod | httpStatus
+            'POST'     | HttpServletResponse.SC_FORBIDDEN
+            'PUT'      | HttpServletResponse.SC_FORBIDDEN
+            'PATCH'    | HttpServletResponse.SC_FORBIDDEN
+            'DELETE'   | HttpServletResponse.SC_FORBIDDEN
+            'INVALID'  | HttpServletResponse.SC_FORBIDDEN
+            'GET'      | HttpServletResponse.SC_OK
+            'HEAD'     | HttpServletResponse.SC_OK
+            'TRACE'    | HttpServletResponse.SC_OK
+            'OPTIONS'  | HttpServletResponse.SC_OK
+    }
+
+    def "csrf default creates CsrfRequestDataValueProcessor"() {
+        when:
+            loadConfig(CsrfAppliedDefaultConfig)
+        then:
+            context.getBean(CsrfRequestDataValueProcessor)
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class CsrfAppliedDefaultConfig extends WebSecurityConfigurerAdapter {
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+        }
+    }
+
+    def "csrf disable"() {
+        setup:
+            loadConfig(DisableCsrfConfig)
+            request.method = "POST"
+            clearCsrfToken()
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            !findFilter(CsrfFilter)
+            response.status == HttpServletResponse.SC_OK
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class DisableCsrfConfig extends WebSecurityConfigurerAdapter {
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .csrf().disable()
+        }
+    }
+
+    def "csrf requireCsrfProtectionMatcher"() {
+        setup:
+            RequireCsrfProtectionMatcherConfig.matcher = Mock(RequestMatcher)
+            RequireCsrfProtectionMatcherConfig.matcher.matches(_) >>> [false,true]
+            loadConfig(RequireCsrfProtectionMatcherConfig)
+            clearCsrfToken()
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_OK
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_FORBIDDEN
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class RequireCsrfProtectionMatcherConfig extends WebSecurityConfigurerAdapter {
+        static RequestMatcher matcher
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .csrf()
+                    .requireCsrfProtectionMatcher(matcher)
+        }
+    }
+
+    def "csrf csrfTokenRepository"() {
+        setup:
+            CsrfTokenRepositoryConfig.repo = Mock(CsrfTokenRepository)
+            loadConfig(CsrfTokenRepositoryConfig)
+            clearCsrfToken()
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            1 * CsrfTokenRepositoryConfig.repo.loadToken(_) >> csrfToken
+            response.status == HttpServletResponse.SC_OK
+    }
+
+    def "csrf clears on logout"() {
+        setup:
+            CsrfTokenRepositoryConfig.repo = Mock(CsrfTokenRepository)
+            1 * CsrfTokenRepositoryConfig.repo.loadToken(_) >> csrfToken
+            loadConfig(CsrfTokenRepositoryConfig)
+            login()
+            request.method = "POST"
+            request.servletPath = "/logout"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            1 *  CsrfTokenRepositoryConfig.repo.saveToken(null, _, _)
+    }
+
+    def "csrf clears on login"() {
+        setup:
+            CsrfTokenRepositoryConfig.repo = Mock(CsrfTokenRepository)
+            1 * CsrfTokenRepositoryConfig.repo.loadToken(_) >> csrfToken
+            loadConfig(CsrfTokenRepositoryConfig)
+            request.method = "POST"
+            request.getSession()
+            request.servletPath = "/login"
+            request.setParameter("username", "user")
+            request.setParameter("password", "password")
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.redirectedUrl == "/"
+            1 *  CsrfTokenRepositoryConfig.repo.saveToken(null, _, _)
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class CsrfTokenRepositoryConfig extends WebSecurityConfigurerAdapter {
+        static CsrfTokenRepository repo
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .formLogin()
+                    .and()
+                .csrf()
+                    .csrfTokenRepository(repo)
+        }
+        @Override
+        protected void registerAuthentication(AuthenticationManagerBuilder auth)
+                throws Exception {
+            auth
+                .inMemoryAuthentication()
+                    .withUser("user").password("password").roles("USER")
+        }
+    }
+
+    def "csrf access denied handler"() {
+        setup:
+            AccessDeniedHandlerConfig.deniedHandler = Mock(AccessDeniedHandler)
+            1 * AccessDeniedHandlerConfig.deniedHandler.handle(_, _, _)
+            loadConfig(AccessDeniedHandlerConfig)
+            clearCsrfToken()
+            request.method = "POST"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_OK
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class AccessDeniedHandlerConfig extends WebSecurityConfigurerAdapter {
+        static AccessDeniedHandler deniedHandler
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .exceptionHandling()
+                    .accessDeniedHandler(deniedHandler)
+        }
+    }
+
+    def "formLogin requires CSRF token"() {
+        setup:
+            loadConfig(FormLoginConfig)
+            clearCsrfToken()
+            request.setParameter("username", "user")
+            request.setParameter("password", "password")
+            request.servletPath = "/login"
+            request.method = "POST"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_FORBIDDEN
+            currentAuthentication == null
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class FormLoginConfig extends WebSecurityConfigurerAdapter {
+        static AccessDeniedHandler deniedHandler
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .formLogin()
+        }
+    }
+
+    def "logout requires CSRF token"() {
+        setup:
+            loadConfig(LogoutConfig)
+            clearCsrfToken()
+            login()
+            request.servletPath = "/logout"
+            request.method = "POST"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "logout is not allowed and user is still authenticated"
+            response.status == HttpServletResponse.SC_FORBIDDEN
+            currentAuthentication != null
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class LogoutConfig extends WebSecurityConfigurerAdapter {
+        static AccessDeniedHandler deniedHandler
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .formLogin()
+        }
+    }
+
+    def "csrf disables POST requests from RequestCache"() {
+        setup:
+            CsrfDisablesPostRequestFromRequestCacheConfig.repo = Mock(CsrfTokenRepository)
+            loadConfig(CsrfDisablesPostRequestFromRequestCacheConfig)
+            request.servletPath = "/some-url"
+            request.requestURI = "/some-url"
+            request.method = "POST"
+        when: "CSRF passes and our session times out"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to the login page"
+            1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "http://localhost/login"
+        when: "authenticate successfully"
+            super.setupWeb(request.session)
+            request.servletPath = "/login"
+            request.setParameter("username","user")
+            request.setParameter("password","password")
+            request.method = "POST"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to default success because we don't want csrf attempts made prior to authentication to pass"
+            1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "/"
+    }
+
+    def "csrf enables GET requests with RequestCache"() {
+        setup:
+            CsrfDisablesPostRequestFromRequestCacheConfig.repo = Mock(CsrfTokenRepository)
+            loadConfig(CsrfDisablesPostRequestFromRequestCacheConfig)
+            request.servletPath = "/some-url"
+            request.requestURI = "/some-url"
+            request.method = "GET"
+        when: "CSRF passes and our session times out"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to the login page"
+            1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "http://localhost/login"
+        when: "authenticate successfully"
+            super.setupWeb(request.session)
+            request.servletPath = "/login"
+            request.setParameter("username","user")
+            request.setParameter("password","password")
+            request.method = "POST"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to original URL since it was a GET"
+            1 * CsrfDisablesPostRequestFromRequestCacheConfig.repo.loadToken(_) >> csrfToken
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "http://localhost/some-url"
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    static class CsrfDisablesPostRequestFromRequestCacheConfig extends WebSecurityConfigurerAdapter {
+        static CsrfTokenRepository repo
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .authorizeRequests()
+                    .anyRequest().authenticated()
+                    .and()
+                .formLogin()
+                    .and()
+                .csrf()
+                    .csrfTokenRepository(repo)
+        }
+        @Override
+        protected void registerAuthentication(AuthenticationManagerBuilder auth)
+                throws Exception {
+            auth
+                .inMemoryAuthentication()
+                    .withUser("user").password("password").roles("USER")
+        }
+    }
+
+    def clearCsrfToken() {
+        request.removeAllParameters()
+    }
+}

+ 20 - 20
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultFiltersTests.groovy

@@ -37,7 +37,8 @@ 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.context.request.async.WebAsyncManagerIntegrationFilter;
+import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
+import org.springframework.security.web.csrf.CsrfFilter
 import org.springframework.security.web.header.HeaderWriterFilter
 import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
 import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
@@ -107,17 +108,17 @@ class DefaultFiltersTests extends BaseSpringSpec {
 
     def "FilterChainProxyBuilder ignoring resources"() {
         when:
-        context = new AnnotationConfigApplicationContext(FilterChainProxyBuilderIgnoringConfig)
+            loadConfig(FilterChainProxyBuilderIgnoringConfig)
         then:
-        List<DefaultSecurityFilterChain> filterChains = context.getBean(FilterChainProxy).filterChains
-        filterChains.size() == 2
-        filterChains[0].requestMatcher.pattern == '/resources/**'
-        filterChains[0].filters.empty
-        filterChains[1].requestMatcher instanceof AnyRequestMatcher
-        filterChains[1].filters.collect { it.class } ==
-                [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, LogoutFilter, RequestCacheAwareFilter,
-                 SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter,
-                 ExceptionTranslationFilter, FilterSecurityInterceptor ]
+            List<DefaultSecurityFilterChain> filterChains = context.getBean(FilterChainProxy).filterChains
+            filterChains.size() == 2
+            filterChains[0].requestMatcher.pattern == '/resources/**'
+            filterChains[0].filters.empty
+            filterChains[1].requestMatcher instanceof AnyRequestMatcher
+            filterChains[1].filters.collect { it.class } ==
+                    [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, RequestCacheAwareFilter,
+                     SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, SessionManagementFilter,
+                     ExceptionTranslationFilter, FilterSecurityInterceptor ]
     }
 
     @Configuration
@@ -139,17 +140,16 @@ class DefaultFiltersTests extends BaseSpringSpec {
 
    def "DefaultFilters.permitAll()"() {
         when:
-        context = new AnnotationConfigApplicationContext(DefaultFiltersConfigPermitAll)
+            loadConfig(DefaultFiltersConfigPermitAll)
+            MockHttpServletResponse response = new MockHttpServletResponse()
+            request = new MockHttpServletRequest(servletPath : uri, queryString: query, method:"POST")
+            setupCsrf()
+            springSecurityFilterChain.doFilter(request, response, new MockFilterChain())
         then:
-        FilterChainProxy filterChain = context.getBean(FilterChainProxy)
-
-        expect:
-        MockHttpServletResponse response = new MockHttpServletResponse()
-        filterChain.doFilter(new MockHttpServletRequest(servletPath : uri, queryString: query), response, new MockFilterChain())
-        response.redirectedUrl == null
+            response.redirectedUrl == "/login?logout"
         where:
-        uri | query
-        "/logout" | null
+            uri | query
+            "/logout" | null
     }
 
     @Configuration

+ 12 - 23
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy

@@ -42,28 +42,16 @@ import org.springframework.security.web.authentication.ui.DefaultLoginPageViewFi
  *
  */
 public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
-    FilterChainProxy springSecurityFilterChain
-    MockHttpServletRequest request
-    MockHttpServletResponse response
-    MockFilterChain chain
-
-    def setup() {
-        request = new MockHttpServletRequest(method:"GET")
-        response = new MockHttpServletResponse()
-        chain = new MockFilterChain()
-    }
-
     def "http/form-login default login generating page"() {
         setup:
             loadConfig(DefaultLoginPageConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
             findFilter(DefaultLoginPageViewFilter)
             response.getRedirectedUrl() == "http://localhost/login"
         when: "request the login page"
-            setup()
+            super.setup()
             request.requestURI = "/login"
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
@@ -73,10 +61,11 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
     <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
     <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
+    <input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
   </table>
 </form></body></html>"""
         when: "fail to log in"
-            setup()
+            super.setup()
             request.servletPath = "/login"
             request.method = "POST"
             springSecurityFilterChain.doFilter(request,response,chain)
@@ -84,7 +73,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
             response.getRedirectedUrl() == "/login?error"
         when: "request the error page"
             HttpSession session = request.session
-            setup()
+            super.setup()
             request.session = session
             request.requestURI = "/login"
             request.queryString = "error"
@@ -96,10 +85,11 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
     <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
     <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
+    <input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
   </table>
 </form></body></html>"""
         when: "login success"
-            setup()
+            super.setup()
             request.servletPath = "/login"
             request.method = "POST"
             request.parameters.username = ["user"] as String[]
@@ -112,7 +102,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     def "logout success renders"() {
         setup:
             loadConfig(DefaultLoginPageConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when: "logout success"
             request.requestURI = "/login"
             request.queryString = "logout"
@@ -125,6 +114,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
     <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
     <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
+    <input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
   </table>
 </form></body></html>"""
     }
@@ -144,7 +134,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     def "custom logout success handler prevents rendering"() {
         setup:
             loadConfig(DefaultLoginPageCustomLogoutSuccessHandlerConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when: "logout success"
             request.requestURI = "/login"
             request.queryString = "logout"
@@ -172,7 +161,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     def "custom logout success url prevents rendering"() {
         setup:
             loadConfig(DefaultLoginPageCustomLogoutConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when: "logout success"
             request.requestURI = "/login"
             request.queryString = "logout"
@@ -200,9 +188,8 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     def "http/form-login default login with remember me"() {
         setup:
             loadConfig(DefaultLoginPageWithRememberMeConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when: "request the login page"
-            setup()
+            super.setup()
             request.requestURI = "/login"
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
@@ -213,6 +200,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
     <tr><td><input type='checkbox' name='remember-me'/></td><td>Remember me on this computer.</td></tr>
     <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
+    <input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
   </table>
 </form></body></html>"""
     }
@@ -234,7 +222,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     def "http/form-login default login with openid"() {
         setup:
             loadConfig(DefaultLoginPageWithOpenIDConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when: "request the login page"
             request.requestURI = "/login"
             springSecurityFilterChain.doFilter(request,response,chain)
@@ -244,6 +231,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     <tr><td>Identity:</td><td><input type='text' size='30' name='openid_identifier'/></td></tr>
     <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
   </table>
+    <input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
 </form></body></html>"""
     }
 
@@ -262,7 +250,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     def "http/form-login default login with openid, form login, and rememberme"() {
         setup:
             loadConfig(DefaultLoginPageWithFormLoginOpenIDRememberMeConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when: "request the login page"
             request.requestURI = "/login"
             springSecurityFilterChain.doFilter(request,response,chain)
@@ -274,6 +261,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
     <tr><td><input type='checkbox' name='remember-me'/></td><td>Remember me on this computer.</td></tr>
     <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
+    <input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
   </table>
 </form><h3>Login with OpenID Identity</h3><form name='oidf' action='/login/openid' method='POST'>
  <table>
@@ -281,6 +269,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
     <tr><td><input type='checkbox' name='remember-me'></td><td>Remember me on this computer.</td></tr>
     <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
   </table>
+    <input name="${csrfToken.parameterName}" type="hidden" value="${csrfToken.token}" />
 </form></body></html>"""
     }
 

+ 8 - 5
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy

@@ -41,6 +41,7 @@ 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.context.request.async.WebAsyncManagerIntegrationFilter
+import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.security.web.header.HeaderWriterFilter
 import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
 import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
@@ -64,7 +65,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 } ==
-                    [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, LogoutFilter, UsernamePasswordAuthenticationFilter,
+                    [WebAsyncManagerIntegrationFilter, SecurityContextPersistenceFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter,
                      RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter,
                      AnonymousAuthenticationFilter, SessionManagementFilter, ExceptionTranslationFilter, FilterSecurityInterceptor ]
 
@@ -78,7 +79,7 @@ class FormLoginConfigurerTests extends BaseSpringSpec {
             !authFilter.requiresAuthentication(new MockHttpServletRequest(servletPath : "/login", method: "GET"), new MockHttpServletResponse())
 
         and: "SessionFixationProtectionStrategy is configured correctly"
-            SessionFixationProtectionStrategy sessionStrategy = ReflectionTestUtils.getField(authFilter,"sessionStrategy")
+            SessionFixationProtectionStrategy sessionStrategy = ReflectionTestUtils.getField(authFilter,"sessionStrategy").delegateStrategies.find { SessionFixationProtectionStrategy }
             sessionStrategy.migrateSessionAttributes
 
         and: "Exception handling is configured correctly"
@@ -112,11 +113,13 @@ class FormLoginConfigurerTests extends BaseSpringSpec {
     def "FormLogin.permitAll()"() {
         when: "load formLogin() with permitAll"
             context = new AnnotationConfigApplicationContext(FormLoginConfigPermitAll)
-
-        then: "the formLogin URLs are granted access"
             FilterChainProxy filterChain = context.getBean(FilterChainProxy)
             MockHttpServletResponse response = new MockHttpServletResponse()
-            filterChain.doFilter(new MockHttpServletRequest(servletPath : servletPath, requestURI: servletPath, queryString: query, method: method), response, new MockFilterChain())
+            request = new MockHttpServletRequest(servletPath : servletPath, requestURI: servletPath, queryString: query, method: method)
+            setupCsrf()
+
+        then: "the formLogin URLs are granted access"
+            filterChain.doFilter(request, response, new MockFilterChain())
             response.redirectedUrl == redirectUrl
 
         where:

+ 4 - 1
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy

@@ -47,8 +47,11 @@ class LogoutConfigurerTests extends BaseSpringSpec {
     def "invoke logout twice does not override"() {
         when:
             loadConfig(InvokeTwiceDoesNotOverride)
+            request.method = "POST"
+            request.servletPath = "/custom/logout"
+            findFilter(LogoutFilter).doFilter(request,response,chain)
         then:
-            findFilter(LogoutFilter).filterProcessesUrl == "/custom/logout"
+            response.redirectedUrl == "/login?logout"
     }
 
     @Configuration

+ 11 - 24
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.groovy

@@ -39,35 +39,24 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
  *
  */
 public class NamespaceHttpBasicTests extends BaseSpringSpec {
-    FilterChainProxy springSecurityFilterChain
-    MockHttpServletRequest request
-    MockHttpServletResponse response
-    MockFilterChain chain
-
-    def setup() {
-        request = new MockHttpServletRequest()
-        response = new MockHttpServletResponse()
-        chain = new MockFilterChain()
-    }
 
     def "http/http-basic"() {
         setup:
             loadConfig(HttpBasicConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
             response.status == HttpServletResponse.SC_UNAUTHORIZED
         when: "fail to log in"
-            setup()
-            login("user","invalid")
+            super.setup()
+            basicLogin("user","invalid")
             springSecurityFilterChain.doFilter(request,response,chain)
         then: "unauthorized"
             response.status == HttpServletResponse.SC_UNAUTHORIZED
             response.getHeader("WWW-Authenticate") == 'Basic realm="Spring Security Application"'
         when: "login success"
-            setup()
-            login()
+            super.setup()
+            basicLogin()
         then: "sent to default succes page"
             !response.committed
     }
@@ -86,9 +75,8 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec {
     def "http@realm"() {
         setup:
             loadConfig(CustomHttpBasicConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
-            login("user","invalid")
+            basicLogin("user","invalid")
             springSecurityFilterChain.doFilter(request,response,chain)
         then: "unauthorized"
             response.status == HttpServletResponse.SC_UNAUTHORIZED
@@ -109,7 +97,6 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec {
     def "http-basic@authentication-details-source-ref"() {
         when:
             loadConfig(AuthenticationDetailsSourceHttpBasicConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         then:
             findFilter(BasicAuthenticationFilter).authenticationDetailsSource.class == CustomAuthenticationDetailsSource
     }
@@ -128,20 +115,20 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec {
     def "http-basic@entry-point-ref"() {
         setup:
             loadConfig(EntryPointRefHttpBasicConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
             response.status == HttpServletResponse.SC_INTERNAL_SERVER_ERROR
         when: "fail to log in"
-            setup()
-            login("user","invalid")
+            super.setup()
+            basicLogin("user","invalid")
             springSecurityFilterChain.doFilter(request,response,chain)
         then: "custom"
             response.status == HttpServletResponse.SC_INTERNAL_SERVER_ERROR
         when: "login success"
-            setup()
-            login()
+            super.setup()
+            basicLogin()
+            springSecurityFilterChain.doFilter(request,response,chain)
         then: "sent to default succes page"
             !response.committed
     }
@@ -162,7 +149,7 @@ public class NamespaceHttpBasicTests extends BaseSpringSpec {
         }
     }
 
-    def login(String username="user",String password="password") {
+    def basicLogin(String username="user",String password="password") {
         def credentials = username + ":" + password
         request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64())
     }

+ 22 - 10
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpHeadersTests.groovy

@@ -36,6 +36,7 @@ import org.springframework.security.web.util.AnyRequestMatcher
  *
  */
 public class NamespaceHttpHeadersTests extends BaseSpringSpec {
+
     def "http/headers"() {
         setup:
             loadConfig(HeadersDefaultConfig)
@@ -48,7 +49,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
                 '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']
+                'X-XSS-Protection' : '1; mode=block',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -68,7 +70,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
             responseHeaders == ['Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate',
-                'Pragma':'no-cache']
+                'Pragma':'no-cache',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -88,7 +91,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains']
+            responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -107,7 +111,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['Strict-Transport-Security': 'max-age=15768000']
+            responseHeaders == ['Strict-Transport-Security': 'max-age=15768000',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -128,7 +133,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['X-Frame-Options': 'SAMEORIGIN']
+            responseHeaders == ['X-Frame-Options': 'SAMEORIGIN',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -150,7 +156,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['X-Frame-Options': 'ALLOW-FROM https://example.com']
+            responseHeaders == ['X-Frame-Options': 'ALLOW-FROM https://example.com',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
 
@@ -171,7 +178,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['X-XSS-Protection': '1; mode=block']
+            responseHeaders == ['X-XSS-Protection': '1; mode=block',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -191,7 +199,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['X-XSS-Protection': '1']
+            responseHeaders == ['X-XSS-Protection': '1',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -211,7 +220,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['X-Content-Type-Options': 'nosniff']
+            responseHeaders == ['X-Content-Type-Options': 'nosniff',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -233,7 +243,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
-            responseHeaders == ['customHeaderName': 'customHeaderValue']
+            responseHeaders == ['customHeaderName': 'customHeaderValue',
+                'X-CSRF-TOKEN' : csrfToken.token]
     }
 
     @Configuration
@@ -245,4 +256,5 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
                     .addHeaderWriter(new StaticHeadersWriter("customHeaderName", "customHeaderValue"))
         }
     }
+
 }

+ 6 - 16
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.groovy

@@ -71,23 +71,13 @@ import org.springframework.security.web.util.RequestMatcher
  *
  */
 public class NamespaceHttpLogoutTests extends BaseSpringSpec {
-    FilterChainProxy springSecurityFilterChain
-    MockHttpServletRequest request
-    MockHttpServletResponse response
-    MockFilterChain chain
-
-    def setup() {
-        request = new MockHttpServletRequest()
-        response = new MockHttpServletResponse()
-        chain = new MockFilterChain()
-    }
 
     def "http/logout"() {
         setup:
             loadConfig(HttpLogoutConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
             login()
-            request.setRequestURI("/logout")
+            request.servletPath = "/logout"
+            request.method = "POST"
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
@@ -106,9 +96,9 @@ public class NamespaceHttpLogoutTests extends BaseSpringSpec {
     def "http/logout custom"() {
         setup:
             loadConfig(CustomHttpLogoutConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
             login()
-            request.setRequestURI("/custom-logout")
+            request.servletPath = "/custom-logout"
+            request.method = "POST"
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
@@ -135,9 +125,9 @@ public class NamespaceHttpLogoutTests extends BaseSpringSpec {
     def "http/logout@success-handler-ref"() {
         setup:
             loadConfig(SuccessHandlerRefHttpLogoutConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
             login()
-            request.setRequestURI("/logout")
+            request.servletPath = "/logout"
+            request.method = "POST"
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:

+ 3 - 18
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpOpenIDLoginTests.groovy

@@ -44,21 +44,9 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS
  *
  */
 public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
-    FilterChainProxy springSecurityFilterChain
-    MockHttpServletRequest request
-    MockHttpServletResponse response
-    MockFilterChain chain
-
-    def setup() {
-        request = new MockHttpServletRequest()
-        response = new MockHttpServletResponse()
-        chain = new MockFilterChain()
-    }
-
     def "http/openid-login"() {
         when:
             loadConfig(OpenIDLoginConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         then:
             findFilter(OpenIDAuthenticationFilter).consumer.class == OpenID4JavaConsumer
         when:
@@ -66,7 +54,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
         then:
             response.getRedirectedUrl() == "http://localhost/login"
         when: "fail to log in"
-            setup()
+            super.setup()
             request.servletPath = "/login/openid"
             request.method = "POST"
             springSecurityFilterChain.doFilter(request,response,chain)
@@ -89,7 +77,6 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
     def "http/openid-login/attribute-exchange"() {
         when:
             loadConfig(OpenIDLoginAttributeExchangeConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
             OpenID4JavaConsumer consumer = findFilter(OpenIDAuthenticationFilter).consumer
         then:
             consumer.class == OpenID4JavaConsumer
@@ -117,7 +104,7 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
         then:
             response.getRedirectedUrl() == "http://localhost/login"
         when: "fail to log in"
-            setup()
+            super.setup()
             request.servletPath = "/login/openid"
             request.method = "POST"
             springSecurityFilterChain.doFilter(request,response,chain)
@@ -165,13 +152,12 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
     def "http/openid-login custom"() {
         setup:
             loadConfig(OpenIDLoginCustomConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             springSecurityFilterChain.doFilter(request,response,chain)
         then:
             response.getRedirectedUrl() == "http://localhost/authentication/login"
         when: "fail to log in"
-            setup()
+            super.setup()
             request.servletPath = "/authentication/login/process"
             request.method = "POST"
             springSecurityFilterChain.doFilter(request,response,chain)
@@ -200,7 +186,6 @@ public class NamespaceHttpOpenIDLoginTests extends BaseSpringSpec {
         when:
             OpenIDLoginCustomRefsConfig.AUDS = Mock(AuthenticationUserDetailsService)
             loadConfig(OpenIDLoginCustomRefsConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         then: "CustomWebAuthenticationDetailsSource is used"
             findFilter(OpenIDAuthenticationFilter).authenticationDetailsSource.class == CustomWebAuthenticationDetailsSource
             findAuthenticationProvider(OpenIDAuthenticationProvider).userDetailsService == OpenIDLoginCustomRefsConfig.AUDS

+ 0 - 15
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.groovy

@@ -55,22 +55,10 @@ import org.springframework.test.util.ReflectionTestUtils
  *
  */
 public class NamespaceHttpX509Tests extends BaseSpringSpec {
-    FilterChainProxy springSecurityFilterChain
-    MockHttpServletRequest request
-    MockHttpServletResponse response
-    MockFilterChain chain
-
-    def setup() {
-        request = new MockHttpServletRequest()
-        response = new MockHttpServletResponse()
-        chain = new MockFilterChain()
-    }
-
     def "http/x509 can authenticate"() {
         setup:
             X509Certificate certificate = loadCert("rod.cer")
             loadConfig(X509Config)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] )
             springSecurityFilterChain.doFilter(request, response, chain);
@@ -148,7 +136,6 @@ public class NamespaceHttpX509Tests extends BaseSpringSpec {
         setup:
             X509Certificate certificate = loadCert("rodatexampledotcom.cer")
             loadConfig(SubjectPrincipalRegexConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] )
             springSecurityFilterChain.doFilter(request, response, chain);
@@ -182,7 +169,6 @@ public class NamespaceHttpX509Tests extends BaseSpringSpec {
         setup:
             X509Certificate certificate = loadCert("rodatexampledotcom.cer")
             loadConfig(UserDetailsServiceRefConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] )
             springSecurityFilterChain.doFilter(request, response, chain);
@@ -216,7 +202,6 @@ public class NamespaceHttpX509Tests extends BaseSpringSpec {
         setup:
             X509Certificate certificate = loadCert("rodatexampledotcom.cer")
             loadConfig(AuthenticationUserDetailsServiceConfig)
-            springSecurityFilterChain = context.getBean(FilterChainProxy)
         when:
             request.setAttribute("javax.servlet.request.X509Certificate", [certificate] as X509Certificate[] )
             springSecurityFilterChain.doFilter(request, response, chain);

+ 3 - 1
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceRememberMeTests.groovy

@@ -81,8 +81,10 @@ public class NamespaceRememberMeTests extends BaseSpringSpec {
         when: "logout"
             super.setup()
             request.setSession(session)
+            super.setupCsrf()
             request.setCookies(rememberMeCookie)
-            request.requestURI = "/logout"
+            request.servletPath = "/logout"
+            request.method = "POST"
             springSecurityFilterChain.doFilter(request,response,chain)
             rememberMeCookie = getRememberMeCookie()
         then: "logout cookie expired"

+ 9 - 5
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy

@@ -41,7 +41,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
         when:
             loadConfig(SessionManagementConfig)
         then:
-            findFilter(SessionManagementFilter).sessionAuthenticationStrategy instanceof SessionFixationProtectionStrategy
+            findSessionAuthenticationStrategy(SessionFixationProtectionStrategy)
     }
 
     @EnableWebSecurity
@@ -91,7 +91,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
         when:
             loadConfig(RefsSessionManagementConfig)
         then:
-            findFilter(SessionManagementFilter).sessionAuthenticationStrategy ==  RefsSessionManagementConfig.SAS
+            findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it ==  RefsSessionManagementConfig.SAS }
     }
 
     @EnableWebSecurity
@@ -110,7 +110,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
         when:
             loadConfig(SFPNoneSessionManagementConfig)
         then:
-            findFilter(SessionManagementFilter).sessionAuthenticationStrategy.class ==  NullAuthenticatedSessionStrategy
+            findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it instanceof  NullAuthenticatedSessionStrategy }
     }
 
     @EnableWebSecurity
@@ -128,7 +128,7 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
         when:
             loadConfig(SFPMigrateSessionManagementConfig)
         then:
-            findFilter(SessionManagementFilter).sessionAuthenticationStrategy.migrateSessionAttributes
+            findSessionAuthenticationStrategy(SessionFixationProtectionStrategy).migrateSessionAttributes
     }
 
     @EnableWebSecurity
@@ -145,7 +145,11 @@ class NamespaceSessionManagementTests extends BaseSpringSpec {
         when:
             loadConfig(SFPNewSessionSessionManagementConfig)
         then:
-            !findFilter(SessionManagementFilter).sessionAuthenticationStrategy.migrateSessionAttributes
+            !findSessionAuthenticationStrategy(SessionFixationProtectionStrategy).migrateSessionAttributes
+    }
+
+    def findSessionAuthenticationStrategy(def c) {
+        findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it.class.isAssignableFrom(c) }
     }
 
     @EnableWebSecurity

+ 2 - 1
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.groovy

@@ -26,6 +26,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
 import org.springframework.security.web.AuthenticationEntryPoint
 import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
+import org.springframework.security.web.csrf.CsrfLogoutHandler;
 import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
 
 /**
@@ -59,7 +60,7 @@ class ServletApiConfigurerTests extends BaseSpringSpec {
         and: "requestFactory != null"
             filter.requestFactory != null
         and: "logoutHandlers populated"
-            filter.logoutHandlers.collect { it.class } == [SecurityContextLogoutHandler]
+            filter.logoutHandlers.collect { it.class } == [CsrfLogoutHandler, SecurityContextLogoutHandler]
     }
 
     @CompileStatic

+ 5 - 2
config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy

@@ -57,8 +57,11 @@ abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests {
     }
 
     List getFilters(String url) {
-        def fcp = appContext.getBean(BeanIds.FILTER_CHAIN_PROXY);
-        return fcp.getFilters(url)
+        springSecurityFilterChain.getFilters(url)
+    }
+
+    Filter getSpringSecurityFilterChain() {
+        appContext.getBean(BeanIds.FILTER_CHAIN_PROXY)
     }
 
     FilterInvocation createFilterinvocation(String path, String method) {

+ 257 - 0
config/src/test/groovy/org/springframework/security/config/http/CsrfConfigTests.groovy

@@ -0,0 +1,257 @@
+/*
+ * 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 static org.mockito.Mockito.*
+import static org.mockito.Matchers.*
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse
+
+import org.spockframework.compiler.model.WhenBlock;
+import org.springframework.mock.web.MockFilterChain
+import org.springframework.mock.web.MockHttpServletRequest
+import org.springframework.mock.web.MockHttpServletResponse
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurerTests.CsrfTokenRepositoryConfig;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurerTests.RequireCsrfProtectionMatcherConfig
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.csrf.CsrfFilter
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor
+import org.springframework.security.web.util.RequestMatcher
+
+import spock.lang.Unroll
+
+/**
+ *
+ * @author Rob Winch
+ */
+class CsrfConfigTests extends AbstractHttpConfigTests {
+    MockHttpServletRequest request = new MockHttpServletRequest()
+    MockHttpServletResponse response = new MockHttpServletResponse()
+    MockFilterChain chain = new MockFilterChain()
+
+    def 'no http csrf filter by default'() {
+        when:
+            httpAutoConfig {
+            }
+            createAppContext()
+        then:
+            !getFilter(CsrfFilter)
+    }
+
+    @Unroll
+    def 'csrf defaults'() {
+        setup:
+            httpAutoConfig {
+                'csrf'()
+            }
+            createAppContext()
+        when:
+            request.method = httpMethod
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == httpStatus
+        where:
+            httpMethod | httpStatus
+            'POST'     | HttpServletResponse.SC_FORBIDDEN
+            'PUT'      | HttpServletResponse.SC_FORBIDDEN
+            'PATCH'    | HttpServletResponse.SC_FORBIDDEN
+            'DELETE'   | HttpServletResponse.SC_FORBIDDEN
+            'INVALID'  | HttpServletResponse.SC_FORBIDDEN
+            'GET'      | HttpServletResponse.SC_OK
+            'HEAD'     | HttpServletResponse.SC_OK
+            'TRACE'    | HttpServletResponse.SC_OK
+            'OPTIONS'  | HttpServletResponse.SC_OK
+    }
+
+    def 'csrf default creates CsrfRequestDataValueProcessor'() {
+        when:
+            httpAutoConfig {
+                'csrf'()
+            }
+            createAppContext()
+        then:
+            appContext.getBean("requestDataValueProcessor",CsrfRequestDataValueProcessor)
+    }
+
+    def 'csrf custom AccessDeniedHandler'() {
+        setup:
+            httpAutoConfig {
+                'access-denied-handler'(ref:'adh')
+                'csrf'()
+            }
+            mockBean(AccessDeniedHandler,'adh')
+            createAppContext()
+            AccessDeniedHandler adh = appContext.getBean(AccessDeniedHandler)
+            request.method = "POST"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            verify(adh).handle(any(HttpServletRequest),any(HttpServletResponse),any(AccessDeniedException))
+            response.status == HttpServletResponse.SC_OK // our mock doesn't do anything
+    }
+
+    def "csrf disables posts for RequestCache"() {
+        setup:
+            httpAutoConfig {
+                'csrf'('token-repository-ref':'repo')
+                'intercept-url'(pattern:"/**",access:'ROLE_USER')
+            }
+            mockBean(CsrfTokenRepository,'repo')
+            createAppContext()
+            CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
+            CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
+            when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
+            request.setParameter(token.parameterName,token.token)
+            request.servletPath = "/some-url"
+            request.requestURI = "/some-url"
+            request.method = "POST"
+        when: "CSRF passes and our session times out"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to the login page"
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "http://localhost/spring_security_login"
+        when: "authenticate successfully"
+            response = new MockHttpServletResponse()
+            request = new MockHttpServletRequest(session: request.session)
+            request.requestURI = "/j_spring_security_check"
+            request.setParameter(token.parameterName,token.token)
+            request.setParameter("j_username","user")
+            request.setParameter("j_password","password")
+            request.method = "POST"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to default success because we don't want csrf attempts made prior to authentication to pass"
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "/"
+    }
+
+    def "csrf enables gets for RequestCache"() {
+        setup:
+            httpAutoConfig {
+                'csrf'('token-repository-ref':'repo')
+                'intercept-url'(pattern:"/**",access:'ROLE_USER')
+            }
+            mockBean(CsrfTokenRepository,'repo')
+            createAppContext()
+            CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
+            CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
+            when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
+            request.setParameter(token.parameterName,token.token)
+            request.servletPath = "/some-url"
+            request.requestURI = "/some-url"
+            request.method = "GET"
+        when: "CSRF passes and our session times out"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to the login page"
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "http://localhost/spring_security_login"
+        when: "authenticate successfully"
+            response = new MockHttpServletResponse()
+            request = new MockHttpServletRequest(session: request.session)
+            request.requestURI = "/j_spring_security_check"
+            request.setParameter(token.parameterName,token.token)
+            request.setParameter("j_username","user")
+            request.setParameter("j_password","password")
+            request.method = "POST"
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then: "sent to original URL since it was a GET"
+            response.status == HttpServletResponse.SC_MOVED_TEMPORARILY
+            response.redirectedUrl == "http://localhost/some-url"
+    }
+
+    def "csrf requireCsrfProtectionMatcher"() {
+        setup:
+            httpAutoConfig {
+                'csrf'('request-matcher-ref':'matcher')
+            }
+            mockBean(RequestMatcher,'matcher')
+            createAppContext()
+            RequestMatcher matcher = appContext.getBean("matcher",RequestMatcher)
+        when:
+            when(matcher.matches(any(HttpServletRequest))).thenReturn(false)
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_OK
+        when:
+            when(matcher.matches(any(HttpServletRequest))).thenReturn(true)
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_FORBIDDEN
+    }
+
+    def "csrf csrfTokenRepository"() {
+        setup:
+            httpAutoConfig {
+                'csrf'('token-repository-ref':'repo')
+            }
+            mockBean(CsrfTokenRepository,'repo')
+            createAppContext()
+            CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
+            CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
+            when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
+            request.setParameter(token.parameterName,token.token)
+            request.method = "POST"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_OK
+        when:
+            request.setParameter(token.parameterName,token.token+"INVALID")
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == HttpServletResponse.SC_FORBIDDEN
+    }
+
+    def "csrf clears on login"() {
+        setup:
+            httpAutoConfig {
+                'csrf'('token-repository-ref':'repo')
+            }
+            mockBean(CsrfTokenRepository,'repo')
+            createAppContext()
+            CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
+            CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
+            when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
+            request.setParameter(token.parameterName,token.token)
+            request.method = "POST"
+            request.setParameter("j_username","user")
+            request.setParameter("j_password","password")
+            request.requestURI = "/j_spring_security_check"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            verify(repo).saveToken(eq(null),any(HttpServletRequest), any(HttpServletResponse))
+    }
+
+    def "csrf clears on logout"() {
+        setup:
+            httpAutoConfig {
+                'csrf'('token-repository-ref':'repo')
+            }
+            mockBean(CsrfTokenRepository,'repo')
+            createAppContext()
+            CsrfTokenRepository repo = appContext.getBean("repo",CsrfTokenRepository)
+            CsrfToken token = new CsrfToken("X-CSRF-TOKEN","_csrf", "abc")
+            when(repo.loadToken(any(HttpServletRequest))).thenReturn(token)
+            request.setParameter(token.parameterName,token.token)
+            request.method = "POST"
+            request.requestURI = "/j_spring_security_logout"
+        when:
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            verify(repo).saveToken(eq(null),any(HttpServletRequest), any(HttpServletResponse))
+    }
+}

+ 1 - 3
config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy

@@ -275,9 +275,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
             httpAutoConfig {
                 'session-management'('session-authentication-strategy-ref':'ss')
             }
-            xml.'b:bean'(id: 'ss', 'class': Mockito.class.name, 'factory-method':'mock') {
-                'b:constructor-arg'(value : SessionAuthenticationStrategy.class.name)
-            }
+            mockBean(SessionAuthenticationStrategy,'ss')
             createAppContext()
 
             MockHttpServletRequest request = new MockHttpServletRequest();

+ 85 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerNoWebMvcTests.java

@@ -0,0 +1,85 @@
+/*
+ * 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 static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.powermock.api.mockito.PowerMockito.spy;
+import static org.powermock.api.mockito.PowerMockito.when;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author Rob Winch
+ *
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ClassUtils.class})
+public class CsrfConfigurerNoWebMvcTests {
+    ConfigurableApplicationContext context;
+
+    @After
+    public void teardown() {
+        if(context != null) {
+            context.close();
+        }
+    }
+
+    @Test
+    public void missingDispatcherServletPreventsCsrfRequestDataValueProcessor() {
+        spy(ClassUtils.class);
+        when(ClassUtils.isPresent(eq("org.springframework.web.servlet.DispatcherServlet"), any(ClassLoader.class))).thenReturn(false);
+
+        loadContext(CsrfDefaultsConfig.class);
+
+        assertThat(context.containsBeanDefinition("requestDataValueProcessor")).isFalse();
+    }
+
+    @Test
+    public void findDispatcherServletPreventsCsrfRequestDataValueProcessor() {
+        loadContext(CsrfDefaultsConfig.class);
+
+        assertThat(context.containsBeanDefinition("requestDataValueProcessor")).isTrue();
+    }
+
+    @EnableWebSecurity
+    @Configuration
+    static class CsrfDefaultsConfig extends WebSecurityConfigurerAdapter {
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+        }
+    }
+
+    private void loadContext(Class<?> configs) {
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
+        annotationConfigApplicationContext.register(configs);
+        annotationConfigApplicationContext.refresh();
+        this.context = annotationConfigApplicationContext;
+    }
+}

+ 4 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerServlet31Tests.java

@@ -49,6 +49,8 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextImpl;
 import org.springframework.security.web.context.HttpRequestResponseHolder;
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
 import org.springframework.util.ReflectionUtils;
 
 
@@ -94,6 +96,8 @@ public class SessionManagementConfigurerServlet31Tests {
         request.setMethod("POST");
         request.setParameter("username", "user");
         request.setParameter("password", "password");
+        CsrfToken token = new HttpSessionCsrfTokenRepository().generateAndSaveToken(request, response);
+        request.setParameter(token.getParameterName(),token.getToken());
         when(ReflectionUtils.findMethod(HttpServletRequest.class, "changeSessionId")).thenReturn(method);
 
         loadConfig(SessionManagementDefaultSessionFixationServlet31Config.class);

+ 8 - 0
docs/guides/src/asciidoc/hello-includes/secure-the-application.asc

@@ -89,6 +89,14 @@ The <<security-config-java,`SecurityConfig`>> will:
 * Allow the user with the *Username* _user_ and the *Password* _password_ to authenticate with form based authentication
 * Allow the user with the *Username* _user_ and the *Password* _password_ to authenticate with HTTP basic authentication
 * Allow the user to logout
+* http://en.wikipedia.org/wiki/Cross-site_request_forgery[CSRF attack] prevention
+* http://en.wikipedia.org/wiki/Session_fixation[Session Fixation] protection
+* Security Header integration
+** http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security[HTTP Strict Transport Security] for secure requests
+** http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx[X-Content-Type-Options] integration
+** Cache Control (can be overridden later by your application to allow caching of your static resources)
+** http://msdn.microsoft.com/en-us/library/dd565647(v=vs.85).aspx[X-XSS-Protection] integration
+** X-Frame-Options integration to help prevent http://en.wikipedia.org/wiki/Clickjacking[Clickjacking]
 * Integrate with the following Servlet API methods
 ** http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getRemoteUser()[HttpServletRequest#getRemoteUser()]
 ** http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getUserPrincipal()[HttpServletRequest.html#getUserPrincipal()]

+ 9 - 3
docs/guides/src/asciidoc/hellomvc.asc

@@ -112,10 +112,12 @@ Now that we can view the user name, let's update the application to allow loggin
 [subs="verbatim,quotes"]
 ----
 <div class="nav-collapse collapse">
+  *<c:url var="logoutUrl" value="/logout"/>
+  <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post">
+    <input type="submit" value="Log out" />
+  </form:form>*
   <p class="navbar-text pull-right">
     <c:out value="${pageContext.request.remoteUser}"/>
-    *<c:url var="logoutUrl" value="/logout"/>
-    <a href="${logoutUrl}">Log out</a>*
   </p>
   <ul class="nav">
     <c:url var="inboxUrl" value="/"/>
@@ -125,8 +127,12 @@ Now that we can view the user name, let's update the application to allow loggin
   </ul>
 </div>
 ----
+In order to help protect against http://en.wikipedia.org/wiki/Cross-site_request_forgery[CSRF attacks], by default, Spring Security Java Configuration log out requires:
 
-Refresh the page at http://localhost:8080/sample/ and you will see the log out link. Click the link and see that the application logs you out successfully.
+* the HTTP method must be a POST
+* the CSRF token must be added to the request. Since we are using Spring MVC, the CSRF token is automatically added as a hidden input for you (view the source to see it). If you were not using Spring MVC, you can access the CsrfToken on the ServletRequest using the attribute _csrf
+
+Refresh the page at http://localhost:8080/sample/ and you will see the log out button. Click the button and see that the application logs you out successfully.
 
 include::hello-includes/basic-authentication.asc[]
 

+ 13 - 7
docs/guides/src/asciidoc/helloworld.asc

@@ -1,6 +1,6 @@
 = Hello Spring Security Java Config
 :author: Rob Winch
-:starter-appname: insecure 
+:starter-appname: insecure
 :completed-appname: helloworld-jc
 :verify-starter-app-include: hello-includes/verify-insecure-app.asc
 
@@ -77,18 +77,24 @@ Now that we can view the user name, let's update the application to allow loggin
 <body>
   <div class="container">
     <h1>This is secured!</h1>
-    <c:url var="logoutUrl" value="/logout"/>
-    <p>
-      Hello <b><c:out value="${pageContext.request.remoteUser}"/></b> 
-    </p>
     <p>
-      <a href="${logoutUrl}">Click here</a> to log out.
+      Hello <b><c:out value="${pageContext.request.remoteUser}"/></b>
     </p>
+    <c:url var="logoutUrl" value="/logout"/>
+    <form class="form-inline" action="${logoutUrl}" method="post">
+      <input type="submit" value="Log out" />
+      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
+    </form>
   </div>
 </body>
 ----
 
-Refresh the page at http://localhost:8080/sample/ and you will see the log out link. Click the link and see that the application logs you out successfully.
+In order to help protect against http://en.wikipedia.org/wiki/Cross-site_request_forgery[CSRF attacks], by default, Spring Security Java Configuration log out requires:
+
+* the HTTP method must be a POST
+* the CSRF token must be added to the request You can access it on the ServletRequest using the attribute _csrf as illustrated above. If you were using Spring MVC, the CSRF token is automatically added as a hidden input for you.
+
+Refresh the page at http://localhost:8080/sample/ and you will see the log out button. Click the logout button and see that the application logs you out successfully.
 
 include::hello-includes/basic-authentication.asc[]
 

+ 25 - 0
docs/manual/src/docbook/appendix-namespace.xml

@@ -211,6 +211,7 @@
                 <itemizedlist>
                     <listitem><link xlink:href="#nsa-access-denied-handler">access-denied-handler</link></listitem>
                     <listitem><link xlink:href="#nsa-anonymous">anonymous</link></listitem>
+                    <listitem><link xlink:href="#nsa-csrf">csrf</link></listitem>
                     <listitem><link xlink:href="#nsa-custom-filter">custom-filter</link></listitem>
                     <listitem><link xlink:href="#nsa-expression-handler">expression-handler</link></listitem>
                     <listitem><link xlink:href="#nsa-form-login">form-login</link></listitem>
@@ -518,6 +519,30 @@
                 </section>
             </section>
         </section>
+        <section xml:id="nsa-csrf">
+            <title><literal>&lt;csrf&gt;</literal></title>
+            <para>This element will add <a href="">CSRF</a> to the application. It also updates the default RequestCache
+                to only replay "GET" requests upon successful authentication.</para>
+            <section xml:id="nsa-csrf-parents">
+                <title>Parent Elements of <literal>&lt;csrf&gt;</literal></title>
+                <itemizedlist>
+                    <listitem><link xlink:href="#nsa-http">http</link></listitem>
+                </itemizedlist>
+            </section>
+            <section xml:id="nsa-csrf-attributes">
+                <title><literal>&lt;csrf&gt;</literal> Attributes</title>
+                <section xml:id="nsa-csrf-token-repository-ref">
+                    <title><literal>token-repository-ref</literal></title>
+                    <para>The CsrfTokenRepository to use. The default is
+                        <classname>HttpSessionCsrfTokenRepository</classname>.</para>
+                </section>
+                <section xml:id="nsa-csrf-request-matcher-ref">
+                    <title><literal>request-matcher-ref</literal></title>
+                    <para>The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any
+                        HTTP method except "GET", "TRACE", "HEAD", "OPTIONS".</para>
+                </section>
+            </section>
+        </section>
         <section xml:id="nsa-custom-filter">
             <title><literal>&lt;custom-filter&gt;</literal></title>
             <para>This element is used to add a filter to the filter chain. It doesn't create any

+ 6 - 1
docs/manual/src/docbook/namespace-config.xml

@@ -716,9 +716,14 @@ List&lt;OpenIDAttribute> attributes = token.getAttributes();</programlisting>The
                         </row>
                         <row>
                             <entry>HEADERS_FILTER</entry>
-                            <entry><literal>HeadersFilter</literal> </entry>
+                            <entry><literal>HeaderWriterFilter</literal> </entry>
                             <entry><literal>http/headers</literal></entry>
                         </row>
+                        <row>
+                            <entry>CSRF_FILTER</entry>
+                            <entry><literal>CsrfFilter</literal> </entry>
+                            <entry><literal>http/csrf</literal></entry>
+                        </row>
                         <row>
                             <entry> LOGOUT_FILTER </entry>
                             <entry><literal>LogoutFilter</literal></entry>

+ 6 - 3
samples/concurrency-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,13 +103,16 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
                 <c:url var="composeUrl" value="/?form"/>
                 <li><a href="${composeUrl}">Compose</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 2 - 3
samples/contacts-xml/contacts.gradle

@@ -22,8 +22,7 @@ dependencies {
             "javax.servlet:jstl:$jstlVersion",
             "hsqldb:hsqldb:$hsqlVersion",
             "org.slf4j:jcl-over-slf4j:$slf4jVersion",
-            "ch.qos.logback:logback-classic:$logbackVersion"
-
-    optional "net.sf.ehcache:ehcache:$ehcacheVersion"
+            "ch.qos.logback:logback-classic:$logbackVersion",
+            "net.sf.ehcache:ehcache:$ehcacheVersion"
 
 }

+ 6 - 7
samples/contacts-xml/pom.xml

@@ -123,13 +123,6 @@
       <scope>compile</scope>
       <optional>true</optional>
     </dependency>
-    <dependency>
-      <groupId>net.sf.ehcache</groupId>
-      <artifactId>ehcache</artifactId>
-      <version>1.6.2</version>
-      <scope>compile</scope>
-      <optional>true</optional>
-    </dependency>
     <dependency>
       <groupId>org.apache.tomcat</groupId>
       <artifactId>tomcat-servlet-api</artifactId>
@@ -154,6 +147,12 @@
       <version>1.2</version>
       <scope>runtime</scope>
     </dependency>
+    <dependency>
+      <groupId>net.sf.ehcache</groupId>
+      <artifactId>ehcache</artifactId>
+      <version>1.6.2</version>
+      <scope>runtime</scope>
+    </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>jcl-over-slf4j</artifactId>

+ 1 - 0
samples/contacts-xml/src/main/resources/applicationContext-security.xml

@@ -31,6 +31,7 @@
         <logout logout-success-url="/index.jsp"/>
         <remember-me />
         <headers/>
+        <csrf/>
         <custom-filter ref="switchUserProcessingFilter" position="SWITCH_USER_FILTER"/>
     </http>
 

+ 2 - 0
samples/contacts-xml/src/main/webapp/WEB-INF/jsp/add.jsp

@@ -33,6 +33,8 @@
     <b>Please fix all errors!</b>
   </spring:hasBindErrors>
   <br><br>
+
+  <input type="hidden" name="<c:out value="${_csrf.parameterName}"/>" value="<c:out value="${_csrf.token}"/>"/>
   <input name="execute" type="submit" alignment="center" value="Execute">
 </form>
 <a href="<c:url value="../hello.htm"/>">Home</a>

+ 13 - 12
samples/contacts-xml/src/main/webapp/WEB-INF/jsp/addPermission.jsp

@@ -13,12 +13,12 @@
       <td alignment="right" width="20%">Recipient:</td>
       <spring:bind path="addPermission.recipient">
         <td width="20%">
-		    <select name="<c:out value="${status.expression}"/>">
-		      <c:forEach var="thisRecipient" items="${recipients}">
-		        <option <c:if test="${thisRecipient.key == status.value}">selected</c:if> value="<c:out value="${thisRecipient.key}"/>">
-		        <c:out value="${thisRecipient.value}"/></option>
-			    </c:forEach>
-		    </select>
+            <select name="<c:out value="${status.expression}"/>">
+              <c:forEach var="thisRecipient" items="${recipients}">
+                <option <c:if test="${thisRecipient.key == status.value}">selected</c:if> value="<c:out value="${thisRecipient.key}"/>">
+                <c:out value="${thisRecipient.value}"/></option>
+                </c:forEach>
+            </select>
         </td>
         <td width="60%">
           <font color="red"><c:out value="${status.errorMessage}"/></font>
@@ -29,12 +29,12 @@
       <td alignment="right" width="20%">Permission:</td>
       <spring:bind path="addPermission.permission">
         <td width="20%">
-		    <select name="<c:out value="${status.expression}"/>">
-		      <c:forEach var="thisPermission" items="${permissions}">
-		        <option <c:if test="${thisPermission.key == status.value}">selected</c:if> value="<c:out value="${thisPermission.key}"/>">
-		        <c:out value="${thisPermission.value}"/></option>
-			    </c:forEach>
-		    </select>
+            <select name="<c:out value="${status.expression}"/>">
+              <c:forEach var="thisPermission" items="${permissions}">
+                <option <c:if test="${thisPermission.key == status.value}">selected</c:if> value="<c:out value="${thisPermission.key}"/>">
+                <c:out value="${thisPermission.value}"/></option>
+                </c:forEach>
+            </select>
         </td>
         <td width="60%">
           <font color="red"><c:out value="${status.errorMessage}"/></font>
@@ -47,6 +47,7 @@
     <b>Please fix all errors!</b>
   </spring:hasBindErrors>
   <br><br>
+  <input type="hidden" name="<c:out value="${_csrf.parameterName}"/>" value="<c:out value="${_csrf.token}"/>"/>
   <input name="execute" type="submit" alignment="center" value="Execute">
 </form>
 <p>

+ 1 - 0
samples/contacts-xml/src/main/webapp/exitUser.jsp

@@ -33,6 +33,7 @@
          </td></tr>
         <tr><td colspan='2'><input name="exit" type="submit" value="Exit"></td></tr>
       </table>
+      <input type="hidden" name="<c:out value="${_csrf.parameterName}"/>" value="<c:out value="${_csrf.token}"/>"/>
     </form>
   </body>
 </html>

+ 1 - 1
samples/contacts-xml/src/main/webapp/login.jsp

@@ -40,7 +40,7 @@
         <tr><td colspan='2'><input name="submit" type="submit"></td></tr>
         <tr><td colspan='2'><input name="reset" type="reset"></td></tr>
       </table>
-
+      <input type="hidden" name="<c:out value="${_csrf.parameterName}"/>" value="<c:out value="${_csrf.token}"/>"/>
     </form>
 
   </body>

+ 1 - 1
samples/contacts-xml/src/main/webapp/switchUser.jsp

@@ -36,7 +36,7 @@
         <tr><td>User:</td><td><input type='text' name='j_username'></td></tr>
         <tr><td colspan='2'><input name="switch" type="submit" value="Switch to User"></td></tr>
       </table>
-
+      <input type="hidden" name="<c:out value="${_csrf.parameterName}"/>" value="<c:out value="${_csrf.token}"/>"/>
     </form>
 
   </body>

+ 3 - 3
samples/hellomvc-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,10 +103,10 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
               <p class="navbar-text pull-right">
                 <c:out value="${pageContext.request.remoteUser}"/>
-                <c:url var="logoutUrl" value="/logout"/>
-                <a href="${logoutUrl}">Log out</a>
               </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>

+ 5 - 4
samples/helloworld-jc/src/main/webapp/index.jsp

@@ -24,13 +24,14 @@
   <body>
     <div class="container">
       <h1>This is secured!</h1>
-      <c:url var="logoutUrl" value="/logout"/>
       <p>
         Hello <b><c:out value="${pageContext.request.remoteUser}"/></b>
       </p>
-      <p>
-        <a href="${logoutUrl}">Click here</a> to log out.
-      </p>
+      <c:url var="logoutUrl" value="/logout"/>
+      <form class="form-inline" action="${logoutUrl}" method="post">
+          <input type="submit" value="Log out" />
+          <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
+      </form>
     </div>
   </body>
 </html>

+ 6 - 3
samples/inmemory-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,13 +103,16 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
                 <c:url var="composeUrl" value="/?form"/>
                 <li><a href="${composeUrl}">Compose</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 6 - 1
samples/insecuremvc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,6 +103,11 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>

+ 6 - 3
samples/jdbc-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,13 +103,16 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
                 <c:url var="composeUrl" value="/?form"/>
                 <li><a href="${composeUrl}">Compose</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 6 - 3
samples/ldap-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,13 +103,16 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
                 <c:url var="composeUrl" value="/?form"/>
                 <li><a href="${composeUrl}">Compose</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 6 - 6
samples/messages-jc/pom.xml

@@ -127,12 +127,6 @@
       <version>3.2.3.RELEASE</version>
       <scope>compile</scope>
     </dependency>
-    <dependency>
-      <groupId>org.springframework</groupId>
-      <artifactId>spring-core</artifactId>
-      <version>3.2.3.RELEASE</version>
-      <scope>compile</scope>
-    </dependency>
     <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-core</artifactId>
@@ -145,6 +139,12 @@
         </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-core</artifactId>
+      <version>3.2.3.RELEASE</version>
+      <scope>compile</scope>
+    </dependency>
     <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-instrument</artifactId>

+ 6 - 3
samples/openid-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -104,6 +104,11 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
@@ -111,8 +116,6 @@
                 <li><a href="${composeUrl}">Compose</a></li>
                 <c:url var="userUrl" value="/user/"/>
                 <li><a href="${userUrl}">User</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 6 - 3
samples/preauth-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,13 +103,16 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
                 <c:url var="composeUrl" value="/?form"/>
                 <li><a href="${composeUrl}">Compose</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 6 - 3
samples/rememberme-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,13 +103,16 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
                 <c:url var="composeUrl" value="/?form"/>
                 <li><a href="${composeUrl}">Compose</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 6 - 3
samples/x509-jc/src/main/webapp/WEB-INF/decorators/main.jsp

@@ -75,7 +75,7 @@
       a {
           color: green;
       }
-      .navbar-text a {
+      .navbar-form {
         margin-left: 1em;
       }
     </style>
@@ -103,13 +103,16 @@
             <c:url var="logoUrl" value="/resources/img/logo.png"/>
             <a class="brand" href="${homeUrl}"><img src="${logoUrl}" alt="Spring Security Sample"/></a>
             <div class="nav-collapse collapse">
+              <c:url var="logoutUrl" value="/logout"/>
+              <form:form class="navbar-form pull-right" action="${logoutUrl}" method="post"><input type="submit" value="Log out" /></form:form>
+              <p class="navbar-text pull-right">
+                <c:out value="${pageContext.request.remoteUser}"/>
+              </p>
               <ul class="nav">
                 <c:url var="inboxUrl" value="/"/>
                 <li><a href="${inboxUrl}">Inbox</a></li>
                 <c:url var="composeUrl" value="/?form"/>
                 <li><a href="${composeUrl}">Compose</a></li>
-                <c:url var="logoutUrl" value="/logout"/>
-                <li><a href="${logoutUrl}">Log out</a></li>
               </ul>
             </div>
           </div>

+ 7 - 0
web/pom.xml

@@ -122,6 +122,13 @@
       <scope>compile</scope>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-webmvc</artifactId>
+      <version>3.2.3.RELEASE</version>
+      <scope>compile</scope>
+      <optional>true</optional>
+    </dependency>
     <dependency>
       <groupId>org.apache.tomcat</groupId>
       <artifactId>tomcat-servlet-api</artifactId>

+ 47 - 22
web/src/main/java/org/springframework/security/web/authentication/logout/LogoutFilter.java

@@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletResponse;
 
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.util.RequestMatcher;
 import org.springframework.security.web.util.UrlUtils;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
@@ -49,7 +50,9 @@ public class LogoutFilter extends GenericFilterBean {
 
     //~ Instance fields ================================================================================================
 
-    private String filterProcessesUrl = "/j_spring_security_logout";
+    private String filterProcessesUrl;
+    private RequestMatcher logoutRequestMatcher;
+
     private final List<LogoutHandler> handlers;
     private final LogoutSuccessHandler logoutSuccessHandler;
 
@@ -65,6 +68,7 @@ public class LogoutFilter extends GenericFilterBean {
         this.handlers = Arrays.asList(handlers);
         Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
         this.logoutSuccessHandler = logoutSuccessHandler;
+        setFilterProcessesUrl("/j_spring_security_logout");
     }
 
     public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
@@ -77,6 +81,7 @@ public class LogoutFilter extends GenericFilterBean {
             urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
         }
         logoutSuccessHandler = urlLogoutSuccessHandler;
+        setFilterProcessesUrl("/j_spring_security_logout");
     }
 
     //~ Methods ========================================================================================================
@@ -114,35 +119,55 @@ public class LogoutFilter extends GenericFilterBean {
      * @return <code>true</code> if logout should occur, <code>false</code> otherwise
      */
     protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) {
-        String uri = request.getRequestURI();
-        int pathParamIndex = uri.indexOf(';');
-
-        if (pathParamIndex > 0) {
-            // strip everything from the first semi-colon
-            uri = uri.substring(0, pathParamIndex);
-        }
-
-        int queryParamIndex = uri.indexOf('?');
-
-        if (queryParamIndex > 0) {
-            // strip everything from the first question mark
-            uri = uri.substring(0, queryParamIndex);
-        }
-
-        if ("".equals(request.getContextPath())) {
-            return uri.endsWith(filterProcessesUrl);
-        }
+        return logoutRequestMatcher.matches(request);
+    }
 
-        return uri.endsWith(request.getContextPath() + filterProcessesUrl);
+    public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
+        Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
+        this.logoutRequestMatcher = logoutRequestMatcher;
     }
 
+    @Deprecated
     public void setFilterProcessesUrl(String filterProcessesUrl) {
-        Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid value for" +
-                " 'filterProcessesUrl'");
+        this.logoutRequestMatcher = new FilterProcessUrlRequestMatcher(filterProcessesUrl);
         this.filterProcessesUrl = filterProcessesUrl;
     }
 
+    @Deprecated
     protected String getFilterProcessesUrl() {
         return filterProcessesUrl;
     }
+
+    private static final class FilterProcessUrlRequestMatcher implements RequestMatcher {
+        private final String filterProcessesUrl;
+
+        private FilterProcessUrlRequestMatcher(String filterProcessesUrl) {
+            Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified");
+            Assert.isTrue(UrlUtils.isValidRedirectUrl(filterProcessesUrl), filterProcessesUrl + " isn't a valid redirect URL");
+            this.filterProcessesUrl = filterProcessesUrl;
+        }
+
+        public boolean matches(HttpServletRequest request) {
+            String uri = request.getRequestURI();
+            int pathParamIndex = uri.indexOf(';');
+
+            if (pathParamIndex > 0) {
+                // strip everything from the first semi-colon
+                uri = uri.substring(0, pathParamIndex);
+            }
+
+            int queryParamIndex = uri.indexOf('?');
+
+            if (queryParamIndex > 0) {
+                // strip everything from the first question mark
+                uri = uri.substring(0, queryParamIndex);
+            }
+
+            if ("".equals(request.getContextPath())) {
+                return uri.endsWith(filterProcessesUrl);
+            }
+
+            return uri.endsWith(request.getContextPath() + filterProcessesUrl);
+        }
+    }
 }

+ 1 - 1
web/src/main/java/org/springframework/security/web/authentication/session/CompositeSessionAuthenticationStrategy.java

@@ -67,7 +67,7 @@ public class CompositeSessionAuthenticationStrategy implements SessionAuthentica
                 throw new IllegalArgumentException("delegateStrategies cannot contain null entires. Got " + delegateStrategies);
             }
         }
-        this.delegateStrategies = new ArrayList<SessionAuthenticationStrategy>(delegateStrategies);
+        this.delegateStrategies = delegateStrategies;
     }
 
     /* (non-Javadoc)

+ 11 - 0
web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageViewFilter.java

@@ -27,6 +27,7 @@ import javax.servlet.http.HttpSession;
 
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.csrf.CsrfToken;
 import org.springframework.web.filter.GenericFilterBean;
 
 /**
@@ -164,6 +165,7 @@ public class DefaultLoginPageViewFilter extends GenericFilterBean {
             }
 
             sb.append("    <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
+            renderHiddenInputs(sb, request);
             sb.append("  </table>\n");
             sb.append("</form>");
         }
@@ -181,6 +183,7 @@ public class DefaultLoginPageViewFilter extends GenericFilterBean {
 
             sb.append("    <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
             sb.append("  </table>\n");
+            renderHiddenInputs(sb, request);
             sb.append("</form>");
         }
 
@@ -189,6 +192,14 @@ public class DefaultLoginPageViewFilter extends GenericFilterBean {
         return sb.toString();
     }
 
+    private void renderHiddenInputs(StringBuilder sb, HttpServletRequest request) {
+        CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+
+        if(token != null) {
+            sb.append("    <input name=\""+ token.getParameterName() +"\" type=\"hidden\" value=\""+ token.getToken() +"\" />\n");
+        }
+    }
+
     private boolean isLogoutSuccess(HttpServletRequest request) {
         return logoutSuccessUrl != null && matches(request, logoutSuccessUrl);
     }

+ 56 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java

@@ -0,0 +1,56 @@
+/*
+ * 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.csrf;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.session.SessionAuthenticationException;
+import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.util.Assert;
+
+/**
+ * {@link CsrfAuthenticationStrategy} is in charge of removing the {@link CsrfToken} upon
+ * authenticating. A new {@link CsrfToken} will then be generated by the framework upon
+ * the next request.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class CsrfAuthenticationStrategy implements
+        SessionAuthenticationStrategy {
+
+    private final CsrfTokenRepository csrfTokenRepository;
+
+    /**
+     * Creates a new instance
+     * @param csrfTokenRepository the {@link CsrfTokenRepository} to use
+     */
+    public CsrfAuthenticationStrategy(CsrfTokenRepository csrfTokenRepository) {
+        Assert.notNull(csrfTokenRepository,"csrfTokenRepository cannot be null");
+        this.csrfTokenRepository = csrfTokenRepository;
+    }
+
+    /* (non-Javadoc)
+     * @see org.springframework.security.web.authentication.session.SessionAuthenticationStrategy#onAuthentication(org.springframework.security.core.Authentication, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    public void onAuthentication(Authentication authentication,
+            HttpServletRequest request, HttpServletResponse response)
+            throws SessionAuthenticationException {
+        this.csrfTokenRepository.saveToken(null, request, response);
+    }
+}

+ 141 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java

@@ -0,0 +1,141 @@
+/*
+ * 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.csrf;
+
+import java.io.IOException;
+import java.util.regex.Pattern;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.access.AccessDeniedHandlerImpl;
+import org.springframework.security.web.util.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * <p>
+ * Applies <a
+ * href="https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)"
+ * >CSRF</a> protection using a synchronizer token pattern. Developers are
+ * required to ensure that {@link CsrfFilter} is invoked for any request that
+ * allows state to change. Typically this just means that they should ensure
+ * their web application follows proper REST semantics (i.e. do not change state
+ * with the HTTP methods GET, HEAD, TRACE, OPTIONS).
+ * </p>
+ *
+ * <p>
+ * Typically the {@link CsrfTokenRepository} implementation chooses to store the
+ * {@link CsrfToken} in {@link HttpSession} with
+ * {@link HttpSessionCsrfTokenRepository}. This is preferred to storing the
+ * token in a cookie which.
+ * </p>
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class CsrfFilter extends OncePerRequestFilter {
+    private final CsrfTokenRepository tokenRepository;
+    private RequestMatcher requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher();
+    private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
+
+    public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
+        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
+        this.tokenRepository = csrfTokenRepository;
+    }
+
+    /* (non-Javadoc)
+     * @see org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
+     */
+    @Override
+    protected void doFilterInternal(HttpServletRequest request,
+            HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        CsrfToken csrfToken = tokenRepository.loadToken(request);
+        if(csrfToken == null) {
+            csrfToken = tokenRepository.generateAndSaveToken(request, response);
+        }
+        request.setAttribute(CsrfToken.class.getName(), csrfToken);
+        request.setAttribute(csrfToken.getParameterName(), csrfToken);
+        response.addHeader(csrfToken.getHeaderName(), csrfToken.getToken());
+
+        if(!requireCsrfProtectionMatcher.matches(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        String actualToken = request.getHeader(csrfToken.getHeaderName());
+        if(actualToken == null) {
+            actualToken = request.getParameter(csrfToken.getParameterName());
+        }
+        if(!csrfToken.getToken().equals(actualToken)) {
+            accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
+            return;
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    /**
+     * Specifies a {@link RequestMatcher} that is used to determine if CSRF
+     * protection should be applied. If the {@link RequestMatcher} returns true
+     * for a given request, then CSRF protection is applied.
+     *
+     * <p>
+     * The default is to apply CSRF protection for any HTTP method other than
+     * GET, HEAD, TRACE, OPTIONS.
+     * </p>
+     *
+     * @param requireCsrfProtectionMatcher
+     *            the {@link RequestMatcher} used to determine if CSRF
+     *            protection should be applied.
+     */
+    public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
+        Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
+        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
+    }
+
+
+    /**
+     * Specifies a {@link AccessDeniedHandler} that should be used when CSRF protection fails.
+     *
+     * <p>
+     * The default is to use AccessDeniedHandlerImpl with no arguments.
+     * </p>
+     *
+     * @param accessDeniedHandler
+     *            the {@link AccessDeniedHandler} to use
+     */
+    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
+        Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
+        this.accessDeniedHandler = accessDeniedHandler;
+    }
+
+    private static class DefaultRequiresCsrfMatcher implements RequestMatcher {
+        private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
+
+        /* (non-Javadoc)
+         * @see org.springframework.security.web.util.RequestMatcher#matches(javax.servlet.http.HttpServletRequest)
+         */
+        public boolean matches(HttpServletRequest request) {
+            return !allowedMethods.matcher(request.getMethod()).matches();
+        }
+    }
+}

+ 54 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfLogoutHandler.java

@@ -0,0 +1,54 @@
+/*
+ * 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.csrf;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.util.Assert;
+
+/**
+ * {@link CsrfLogoutHandler} is in charge of removing the {@link CsrfToken} upon
+ * logout. A new {@link CsrfToken} will then be generated by the framework upon
+ * the next request.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class CsrfLogoutHandler implements LogoutHandler {
+    private final CsrfTokenRepository csrfTokenRepository;
+
+    /**
+     * Creates a new instance
+     * @param csrfTokenRepository the {@link CsrfTokenRepository} to use
+     */
+    public CsrfLogoutHandler(CsrfTokenRepository csrfTokenRepository) {
+        Assert.notNull(csrfTokenRepository,"csrfTokenRepository cannot be null");
+        this.csrfTokenRepository = csrfTokenRepository;
+    }
+
+    /**
+     * Clears the {@link CsrfToken}
+     *
+     * @see org.springframework.security.web.authentication.logout.LogoutHandler#logout(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.core.Authentication)
+     */
+    public void logout(HttpServletRequest request,
+            HttpServletResponse response, Authentication authentication) {
+        this.csrfTokenRepository.saveToken(null, request, response);
+    }
+}

+ 78 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfToken.java

@@ -0,0 +1,78 @@
+/*
+ * 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.csrf;
+
+import java.io.Serializable;
+
+import org.springframework.util.Assert;
+
+/**
+ * A CSRF token that is used to protect against CSRF attacks.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+@SuppressWarnings("serial")
+public final class CsrfToken implements Serializable {
+
+    private final String token;
+
+    private final String parameterName;
+
+    private final String headerName;
+
+    /**
+     * Creates a new instance
+     * @param headerName the HTTP header name to use
+     * @param parameterName the HTTP parameter name to use
+     * @param token the value of the token (i.e. expected value of the HTTP parameter of parametername).
+     */
+    public CsrfToken(String headerName, String parameterName, String token) {
+        Assert.hasLength(headerName, "headerName cannot be null or empty");
+        Assert.hasLength(parameterName, "parameterName cannot be null or empty");
+        Assert.hasLength(token, "token cannot be null or empty");
+        this.headerName = headerName;
+        this.parameterName = parameterName;
+        this.token = token;
+    }
+
+    /**
+     * Gets the HTTP header that the CSRF is populated on the response and can
+     * be placed on requests instead of the parameter. Cannot be null.
+     *
+     * @return the HTTP header that the CSRF is populated on the response and
+     *         can be placed on requests instead of the parameter
+     */
+    public String getHeaderName() {
+        return headerName;
+    }
+
+    /**
+     * Gets the HTTP parameter name that should contain the token. Cannot be null.
+     * @return the HTTP parameter name that should contain the token.
+     */
+    public String getParameterName() {
+        return parameterName;
+    }
+
+    /**
+     * Gets the token value. Cannot be null.
+     * @return the token value
+     */
+    public String getToken() {
+        return token;
+    }
+}

+ 71 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRepository.java

@@ -0,0 +1,71 @@
+/*
+ * 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.csrf;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * An API to allow changing the method in which the expected {@link CsrfToken}
+ * is associated to the {@link HttpServletRequest}. For example, it may be
+ * stored in {@link HttpSession}.
+ *
+ * @see HttpSessionCsrfTokenRepository
+ *
+ * @author Rob Winch
+ * @since 3.2
+ *
+ */
+public interface CsrfTokenRepository {
+
+    /**
+     * Generates and saves the expected {@link CsrfToken}
+     *
+     * @param request
+     *            the {@link HttpServletRequest} to use
+     * @param response
+     *            the {@link HttpServletResponse} to use
+     * @return the {@link CsrfToken} that was generated and saved. Cannot be
+     *         null.
+     */
+    CsrfToken generateAndSaveToken(HttpServletRequest request,
+            HttpServletResponse response);
+
+    /**
+     * Saves the {@link CsrfToken} using the {@link HttpServletRequest} and
+     * {@link HttpServletResponse}. If the {@link CsrfToken} is null, it is the
+     * same as deleting it.
+     *
+     * @param token
+     *            the {@link CsrfToken} to save or null to delete
+     * @param request
+     *            the {@link HttpServletRequest} to use
+     * @param response
+     *            the {@link HttpServletResponse} to use
+     */
+    void saveToken(CsrfToken token, HttpServletRequest request,
+            HttpServletResponse response);
+
+    /**
+     * Loads the expected {@link CsrfToken} from the {@link HttpServletRequest}
+     *
+     * @param request
+     *            the {@link HttpServletRequest} to use
+     * @return the {@link CsrfToken} or null if none exists
+     */
+    CsrfToken loadToken(HttpServletRequest request);
+}

+ 109 - 0
web/src/main/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.java

@@ -0,0 +1,109 @@
+/*
+ * 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.csrf;
+
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.springframework.util.Assert;
+
+/**
+ * A {@link CsrfTokenRepository} that stores the {@link CsrfToken} in the {@link HttpSession}.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
+    private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
+
+    private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
+
+    private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
+
+    private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
+
+    private String headerName = DEFAULT_CSRF_HEADER_NAME;
+
+    private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
+
+    /*
+     * (non-Javadoc)
+     * @see org.springframework.security.web.csrf.CsrfTokenRepository#saveToken(org.springframework.security.web.csrf.CsrfToken, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    public void saveToken(CsrfToken token, HttpServletRequest request,
+            HttpServletResponse response) {
+        HttpSession session = request.getSession();
+        if(token == null) {
+            session.removeAttribute(sessionAttributeName);
+        } else {
+            session.setAttribute(sessionAttributeName, token);
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.springframework.security.web.csrf.CsrfTokenRepository#loadToken(javax.servlet.http.HttpServletRequest)
+     */
+    public CsrfToken loadToken(HttpServletRequest request) {
+        return (CsrfToken) request.getSession().getAttribute(sessionAttributeName);
+    }
+
+    /* (non-Javadoc)
+     * @see org.springframework.security.web.csrf.CsrfTokenRepository#generateNewToken(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    public CsrfToken generateAndSaveToken(HttpServletRequest request,
+            HttpServletResponse response) {
+        CsrfToken token = new CsrfToken(headerName, parameterName, createNewToken());
+        saveToken(token, request, response);
+        return token;
+    }
+
+    /**
+     * Sets the {@link HttpServletRequest} parameter name that the {@link CsrfToken} is expected to appear on
+     * @param parameterName the new parameter name to use
+     */
+    public void setParameterName(String parameterName) {
+        Assert.hasLength(parameterName, "parameterName cannot be null or empty");
+        this.parameterName = parameterName;
+    }
+
+    /**
+     * Sets the header name that the {@link CsrfToken} is expected to appear on
+     * and the header that the response will contain the {@link CsrfToken}.
+     *
+     * @param parameterName
+     *            the new parameter name to use
+     */
+    public void setHeaderName(String parameterName) {
+        Assert.hasLength(parameterName, "parameterName cannot be null or empty");
+        this.parameterName = parameterName;
+    }
+
+    /**
+     * Sets the {@link HttpSession} attribute name that the {@link CsrfToken} is stored in
+     * @param sessionAttributeName the new attribute name to use
+     */
+    public void setSessionAttributeName(String sessionAttributeName) {
+        Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty");
+        this.sessionAttributeName = sessionAttributeName;
+    }
+
+    private String createNewToken() {
+        return UUID.randomUUID().toString();
+    }
+}

+ 40 - 0
web/src/main/java/org/springframework/security/web/csrf/InvalidCsrfTokenException.java

@@ -0,0 +1,40 @@
+/*
+ * 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.csrf;
+
+import org.springframework.security.access.AccessDeniedException;
+
+
+/**
+ * Thrown when an invalid or missing {@link CsrfToken} is found in the HttpServletRequest
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+@SuppressWarnings("serial")
+public class InvalidCsrfTokenException extends AccessDeniedException {
+
+    /**
+     * @param msg
+     */
+    public InvalidCsrfTokenException(CsrfToken expectedAccessToken, String actualAccessToken) {
+        super("Invalid CSRF Token '" + actualAccessToken
+                + "' was found on the request parameter '"
+                + expectedAccessToken.getParameterName() + "' or header '"
+                + expectedAccessToken.getHeaderName() + "'.");
+    }
+
+}

+ 86 - 0
web/src/main/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessor.java

@@ -0,0 +1,86 @@
+/*
+ * 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.servlet.support.csrf;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.web.servlet.support.RequestDataValueProcessor;
+
+/**
+ * Integration with Spring Web MVC that automatically adds the {@link CsrfToken}
+ * into forms with hidden inputs when using Spring tag libraries.
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class CsrfRequestDataValueProcessor implements
+        RequestDataValueProcessor {
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see org.springframework.web.servlet.support.RequestDataValueProcessor#
+     * processAction(javax.servlet.http.HttpServletRequest, java.lang.String)
+     */
+    public String processAction(HttpServletRequest request, String action) {
+        return action;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see org.springframework.web.servlet.support.RequestDataValueProcessor#
+     * processFormFieldValue(javax.servlet.http.HttpServletRequest,
+     * java.lang.String, java.lang.String, java.lang.String)
+     */
+    public String processFormFieldValue(HttpServletRequest request,
+            String name, String value, String type) {
+        return value;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see org.springframework.web.servlet.support.RequestDataValueProcessor#
+     * getExtraHiddenFields(javax.servlet.http.HttpServletRequest)
+     */
+    public Map<String, String> getExtraHiddenFields(HttpServletRequest request) {
+        CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class
+                .getName());
+        if (token == null) {
+            return Collections.emptyMap();
+        }
+        Map<String, String> hiddenFields = new HashMap<String, String>(1);
+        hiddenFields.put(token.getParameterName(), token.getToken());
+        return hiddenFields;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see
+     * org.springframework.web.servlet.support.RequestDataValueProcessor#processUrl
+     * (javax.servlet.http.HttpServletRequest, java.lang.String)
+     */
+    public String processUrl(HttpServletRequest request, String url) {
+        return url;
+    }
+}

+ 64 - 0
web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java

@@ -0,0 +1,64 @@
+/*
+ * 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.csrf;
+
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+
+/**
+ * @author Rob Winch
+ *
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CsrfAuthenticationStrategyTests {
+    @Mock
+    private CsrfTokenRepository csrfTokenRepository;
+
+    private MockHttpServletRequest request;
+
+    private MockHttpServletResponse response;
+
+    private CsrfAuthenticationStrategy strategy;
+
+    @Before
+    public void setup() {
+        request = new MockHttpServletRequest();
+        response = new MockHttpServletResponse();
+        strategy = new CsrfAuthenticationStrategy(csrfTokenRepository);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullCsrfTokenRepository() {
+        new CsrfAuthenticationStrategy(null);
+    }
+
+    @Test
+    public void logoutRemovesCsrfToken() {
+        strategy.onAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"),request, response);
+
+        verify(csrfTokenRepository).saveToken(null, request, response);
+    }
+
+}
+

+ 303 - 0
web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java

@@ -0,0 +1,303 @@
+/*
+ * 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.csrf;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.util.RequestMatcher;
+
+/**
+ * @author Rob Winch
+ *
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CsrfFilterTests {
+
+    @Mock
+    private RequestMatcher requestMatcher;
+    @Mock
+    private CsrfTokenRepository tokenRepository;
+    @Mock
+    private FilterChain filterChain;
+    @Mock
+    private AccessDeniedHandler deniedHandler;
+
+    private MockHttpServletRequest request;
+    private MockHttpServletResponse response;
+    private CsrfToken token;
+
+
+    private CsrfFilter filter;
+
+    @Before
+    public void setup() {
+        token = new CsrfToken("headerName","paramName", "csrfTokenValue");
+        resetRequestResponse();
+        filter = new CsrfFilter(tokenRepository);
+        filter.setRequireCsrfProtectionMatcher(requestMatcher);
+        filter.setAccessDeniedHandler(deniedHandler);
+    }
+
+    private void resetRequestResponse() {
+        request = new MockHttpServletRequest();
+        response = new MockHttpServletResponse();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullRepository() {
+        new CsrfFilter(null);
+    }
+
+    @Test
+    public void doFilterAccessDeniedNoTokenPresent() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class));
+        verifyZeroInteractions(filterChain);
+    }
+
+    @Test
+    public void doFilterAccessDeniedIncorrectTokenPresent() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+        request.setParameter(token.getParameterName(), token.getToken()+ " INVALID");
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class));
+        verifyZeroInteractions(filterChain);
+    }
+
+    @Test
+    public void doFilterAccessDeniedIncorrectTokenPresentHeader() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+        request.addHeader(token.getHeaderName(), token.getToken()+ " INVALID");
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class));
+        verifyZeroInteractions(filterChain);
+    }
+
+    @Test
+    public void doFilterAccessDeniedIncorrectTokenPresentHeaderPreferredOverParameter() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+        request.setParameter(token.getParameterName(), token.getToken());
+        request.addHeader(token.getHeaderName(), token.getToken()+ " INVALID");
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class));
+        verifyZeroInteractions(filterChain);
+    }
+
+    @Test
+    public void doFilterNotCsrfRequestExistingToken() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(false);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(filterChain).doFilter(request, response);
+        verifyZeroInteractions(deniedHandler);
+    }
+
+    @Test
+    public void doFilterNotCsrfRequestGenerateToken() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(false);
+        when(tokenRepository.generateAndSaveToken(request, response)).thenReturn(token);
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(filterChain).doFilter(request, response);
+        verifyZeroInteractions(deniedHandler);
+    }
+
+    @Test
+    public void doFilterIsCsrfRequestExistingTokenHeader() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+        request.addHeader(token.getHeaderName(), token.getToken());
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(filterChain).doFilter(request, response);
+        verifyZeroInteractions(deniedHandler);
+    }
+
+    @Test
+    public void doFilterIsCsrfRequestExistingTokenHeaderPreferredOverInvalidParam() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+        request.setParameter(token.getParameterName(), token.getToken()+ " INVALID");
+        request.addHeader(token.getHeaderName(), token.getToken());
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(filterChain).doFilter(request, response);
+        verifyZeroInteractions(deniedHandler);
+    }
+
+    @Test
+    public void doFilterIsCsrfRequestExistingToken() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+        request.setParameter(token.getParameterName(), token.getToken());
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(filterChain).doFilter(request, response);
+        verifyZeroInteractions(deniedHandler);
+    }
+
+    @Test
+    public void doFilterIsCsrfRequestGenerateToken() throws ServletException, IOException {
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.generateAndSaveToken(request, response)).thenReturn(token);
+        request.setParameter(token.getParameterName(), token.getToken());
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        verify(filterChain).doFilter(request, response);
+        verifyZeroInteractions(deniedHandler);
+    }
+
+    @Test
+    public void doFilterDefaultRequireCsrfProtectionMatcherAllowedMethods() throws ServletException, IOException {
+        filter = new CsrfFilter(tokenRepository);
+        filter.setAccessDeniedHandler(deniedHandler);
+
+        for(String method : Arrays.asList("GET","TRACE", "OPTIONS", "HEAD")) {
+            resetRequestResponse();
+            when(tokenRepository.loadToken(request)).thenReturn(token);
+            request.setMethod(method);
+
+            filter.doFilter(request, response, filterChain);
+
+            verify(filterChain).doFilter(request, response);
+            verifyZeroInteractions(deniedHandler);
+        }
+    }
+
+    @Test
+    public void doFilterDefaultRequireCsrfProtectionMatcherDeniedMethods() throws ServletException, IOException {
+        filter = new CsrfFilter(tokenRepository);
+        filter.setAccessDeniedHandler(deniedHandler);
+
+        for(String method : Arrays.asList("POST","PUT", "PATCH", "DELETE", "INVALID")) {
+            resetRequestResponse();
+            when(tokenRepository.loadToken(request)).thenReturn(token);
+            request.setMethod(method);
+
+            filter.doFilter(request, response, filterChain);
+
+            verify(deniedHandler).handle(eq(request), eq(response), any(InvalidCsrfTokenException.class));
+            verifyZeroInteractions(filterChain);
+        }
+    }
+
+    @Test
+    public void doFilterDefaultAccessDenied() throws ServletException, IOException {
+        filter = new CsrfFilter(tokenRepository);
+        filter.setRequireCsrfProtectionMatcher(requestMatcher);
+        when(requestMatcher.matches(request)).thenReturn(true);
+        when(tokenRepository.loadToken(request)).thenReturn(token);
+
+        filter.doFilter(request, response, filterChain);
+
+        assertThat(response.getHeader(token.getHeaderName())).isEqualTo(token.getToken());
+        assertThat(request.getAttribute(token.getParameterName())).isEqualTo(token);
+        assertThat(request.getAttribute(CsrfToken.class.getName())).isEqualTo(token);
+
+        assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN);
+        verifyZeroInteractions(filterChain);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setRequireCsrfProtectionMatcherNull() {
+        filter.setRequireCsrfProtectionMatcher(null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setAccessDeniedHandlerNull() {
+        filter.setAccessDeniedHandler(null);
+    }
+}

+ 63 - 0
web/src/test/java/org/springframework/security/web/csrf/CsrfLogoutHandlerTests.java

@@ -0,0 +1,63 @@
+/*
+ * 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.csrf;
+
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+
+/**
+ * @author Rob Winch
+ * @since 3.2
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CsrfLogoutHandlerTests {
+    @Mock
+    private CsrfTokenRepository csrfTokenRepository;
+
+    private MockHttpServletRequest request;
+
+    private MockHttpServletResponse response;
+
+    private CsrfLogoutHandler handler;
+
+    @Before
+    public void setup() {
+        request = new MockHttpServletRequest();
+        response = new MockHttpServletResponse();
+        handler = new CsrfLogoutHandler(csrfTokenRepository);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullCsrfTokenRepository() {
+        new CsrfLogoutHandler(null);
+    }
+
+    @Test
+    public void logoutRemovesCsrfToken() {
+        handler.logout(request, response, new TestingAuthenticationToken("user", "password", "ROLE_USER"));
+
+        verify(csrfTokenRepository).saveToken(null, request, response);
+    }
+
+}

+ 58 - 0
web/src/test/java/org/springframework/security/web/csrf/CsrfTokenTests.java

@@ -0,0 +1,58 @@
+/*
+ * 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.csrf;
+
+import org.junit.Test;
+
+/**
+ * @author Rob Winch
+ *
+ */
+public class CsrfTokenTests {
+    private final String headerName = "headerName";
+    private final String parameterName = "parameterName";
+    private final String tokenValue = "tokenValue";
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullHeaderName() {
+        new CsrfToken(null,parameterName, tokenValue);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorEmptyHeaderName() {
+        new CsrfToken("",parameterName, tokenValue);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullParameterName() {
+        new CsrfToken(headerName,null, tokenValue);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorEmptyParameterName() {
+        new CsrfToken(headerName,"", tokenValue);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNullTokenValue() {
+        new CsrfToken(headerName,parameterName, null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorEmptyTokenValue() {
+        new CsrfToken(headerName,parameterName, "");
+    }
+}

+ 127 - 0
web/src/test/java/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepositoryTests.java

@@ -0,0 +1,127 @@
+/*
+ * 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.csrf;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+/**
+ * @author Rob Winch
+ *
+ */
+public class HttpSessionCsrfTokenRepositoryTests {
+    private MockHttpServletRequest request;
+
+    private MockHttpServletResponse response;
+
+    private CsrfToken token;
+    private HttpSessionCsrfTokenRepository repo;
+
+    @Before
+    public void setup() {
+        request = new MockHttpServletRequest();
+        response = new MockHttpServletResponse();
+        repo = new HttpSessionCsrfTokenRepository();
+    }
+
+    @Test
+    public void generateAndSaveToken() {
+        token = repo.generateAndSaveToken(request, response);
+
+        assertThat(token.getParameterName()).isEqualTo("_csrf");
+        assertThat(token.getToken()).isNotEmpty();
+
+        CsrfToken loadedToken = repo.loadToken(request);
+
+        assertThat(loadedToken).isEqualTo(token);
+    }
+
+    @Test
+    public void generateAndSaveTokenCustomParameter() {
+        String paramName = "_csrf";
+        repo.setParameterName(paramName);
+
+        token = repo.generateAndSaveToken(request, response);
+
+        assertThat(token.getParameterName()).isEqualTo(paramName);
+        assertThat(token.getToken()).isNotEmpty();
+    }
+
+    @Test
+    public void loadTokenNull() {
+        assertThat(repo.loadToken(request)).isNull();
+    }
+
+    @Test
+    public void saveToken() {
+        CsrfToken tokenToSave = new CsrfToken("123", "abc", "def");
+        repo.saveToken(tokenToSave, request, response);
+
+        String attrName = request.getSession().getAttributeNames()
+                .nextElement();
+        CsrfToken loadedToken = (CsrfToken) request.getSession().getAttribute(
+                attrName);
+
+        assertThat(loadedToken).isEqualTo(tokenToSave);
+    }
+
+    @Test
+    public void saveTokenCustomSessionAttribute() {
+        CsrfToken tokenToSave = new CsrfToken("123", "abc", "def");
+        String sessionAttributeName = "custom";
+        repo.setSessionAttributeName(sessionAttributeName);
+        repo.saveToken(tokenToSave, request, response);
+
+        CsrfToken loadedToken = (CsrfToken) request.getSession().getAttribute(
+                sessionAttributeName);
+
+        assertThat(loadedToken).isEqualTo(tokenToSave);
+    }
+
+    @Test
+    public void saveTokenNullToken() {
+        saveToken();
+
+        repo.saveToken(null, request, response);
+
+        assertThat(request.getSession().getAttributeNames().hasMoreElements())
+                .isFalse();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setSessionAttributeNameEmpty() {
+        repo.setSessionAttributeName("");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setSessionAttributeNameNull() {
+        repo.setSessionAttributeName(null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setParameterNameEmpty() {
+        repo.setParameterName("");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setParameterNameNull() {
+        repo.setParameterName(null);
+    }
+}

+ 79 - 0
web/src/test/java/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessorTests.java

@@ -0,0 +1,79 @@
+/*
+ * 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.servlet.support.csrf;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.web.csrf.CsrfToken;
+
+/**
+ * @author Rob Winch
+ *
+ */
+public class CsrfRequestDataValueProcessorTests {
+    private MockHttpServletRequest request;
+
+    private MockHttpServletResponse response;
+
+    private CsrfRequestDataValueProcessor processor;
+
+    @Before
+    public void setup() {
+        request = new MockHttpServletRequest();
+        response = new MockHttpServletResponse();
+        processor = new CsrfRequestDataValueProcessor();
+    }
+
+    @Test
+    public void getExtraHiddenFieldsNoCsrfToken() {
+        assertThat(processor.getExtraHiddenFields(request)).isEmpty();
+    }
+
+    @Test
+    public void getExtraHiddenFieldsHasCsrfToken() {
+        CsrfToken token = new CsrfToken("1", "a", "b");
+        request.setAttribute(CsrfToken.class.getName(), token);
+        Map<String,String> expected = new HashMap<String,String>();
+        expected.put(token.getParameterName(),token.getToken());
+
+        assertThat(processor.getExtraHiddenFields(request)).isEqualTo(expected);
+    }
+
+    @Test
+    public void processAction() {
+        String action = "action";
+        assertThat(processor.processAction(request, action)).isEqualTo(action);
+    }
+
+    @Test
+    public void processFormFieldValue() {
+        String value = "action";
+        assertThat(processor.processFormFieldValue(request, "name", value, "hidden")).isEqualTo(value);
+    }
+
+    @Test
+    public void processUrl() {
+        String url = "url";
+        assertThat(processor.processUrl(request, url)).isEqualTo(url);
+    }
+}

+ 2 - 0
web/web.gradle

@@ -11,6 +11,8 @@ dependencies {
             "org.springframework:spring-tx:$springVersion",
             "org.springframework:spring-web:$springVersion"
 
+    optional "org.springframework:spring-webmvc:$springVersion"
+
     provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     testCompile project(':spring-security-core').sourceSets.test.output,