Browse Source

Add new interfaces for CSRF request processing

Issue gh-4001
Issue gh-11456
Steve Riesenberg 3 years ago
parent
commit
86fbb8db07
18 changed files with 572 additions and 59 deletions
  1. 36 11
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java
  2. 14 6
      config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java
  3. 6 3
      config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc
  4. 12 7
      config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd
  5. 4 1
      config/src/test/java/org/springframework/security/config/annotation/web/configuration/DeferHttpSessionJavaConfigTests.java
  6. 89 1
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java
  7. 1 1
      config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java
  8. 4 1
      config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestAttrName.xml
  9. 3 1
      config/src/test/resources/org/springframework/security/config/http/DeferHttpSessionTests-Explicit.xml
  10. 7 4
      docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc
  11. 14 3
      web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java
  12. 37 18
      web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java
  13. 45 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestAttributeHandler.java
  14. 75 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessor.java
  15. 42 0
      web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestResolver.java
  16. 21 0
      web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java
  17. 28 2
      web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java
  18. 134 0
      web/src/test/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessorTests.java

+ 36 - 11
config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -36,6 +36,8 @@ 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.CsrfTokenRequestAttributeHandler;
+import org.springframework.security.web.csrf.CsrfTokenRequestResolver;
 import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
 import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
 import org.springframework.security.web.csrf.MissingCsrfTokenException;
@@ -89,7 +91,9 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
 
 	private SessionAuthenticationStrategy sessionAuthenticationStrategy;
 
-	private String csrfRequestAttributeName;
+	private CsrfTokenRequestAttributeHandler requestAttributeHandler;
+
+	private CsrfTokenRequestResolver requestResolver;
 
 	private final ApplicationContext context;
 
@@ -127,12 +131,25 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
 	}
 
 	/**
-	 * Sets the {@link CsrfFilter#setCsrfRequestAttributeName(String)}
-	 * @param csrfRequestAttributeName the attribute name to set the CsrfToken on.
-	 * @return the {@link CsrfConfigurer} for further customizations.
+	 * Specify a {@link CsrfTokenRequestAttributeHandler} to use for making the
+	 * {@code CsrfToken} available as a request attribute.
+	 * @param requestAttributeHandler the {@link CsrfTokenRequestAttributeHandler} to use
+	 * @return the {@link CsrfConfigurer} for further customizations
+	 */
+	public CsrfConfigurer<H> csrfTokenRequestAttributeHandler(
+			CsrfTokenRequestAttributeHandler requestAttributeHandler) {
+		this.requestAttributeHandler = requestAttributeHandler;
+		return this;
+	}
+
+	/**
+	 * Specify a {@link CsrfTokenRequestResolver} to use for resolving the token value
+	 * from the request.
+	 * @param requestResolver the {@link CsrfTokenRequestResolver} to use
+	 * @return the {@link CsrfConfigurer} for further customizations
 	 */
-	public CsrfConfigurer<H> csrfRequestAttributeName(String csrfRequestAttributeName) {
-		this.csrfRequestAttributeName = csrfRequestAttributeName;
+	public CsrfConfigurer<H> csrfTokenRequestResolver(CsrfTokenRequestResolver requestResolver) {
+		this.requestResolver = requestResolver;
 		return this;
 	}
 
@@ -214,9 +231,6 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
 	@Override
 	public void configure(H http) {
 		CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
-		if (this.csrfRequestAttributeName != null) {
-			filter.setCsrfRequestAttributeName(this.csrfRequestAttributeName);
-		}
 		RequestMatcher requireCsrfProtectionMatcher = getRequireCsrfProtectionMatcher();
 		if (requireCsrfProtectionMatcher != null) {
 			filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
@@ -233,6 +247,12 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
 		if (sessionConfigurer != null) {
 			sessionConfigurer.addSessionAuthenticationStrategy(getSessionAuthenticationStrategy());
 		}
+		if (this.requestAttributeHandler != null) {
+			filter.setRequestAttributeHandler(this.requestAttributeHandler);
+		}
+		if (this.requestResolver != null) {
+			filter.setRequestResolver(this.requestResolver);
+		}
 		filter = postProcess(filter);
 		http.addFilter(filter);
 	}
@@ -321,7 +341,12 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
 		if (this.sessionAuthenticationStrategy != null) {
 			return this.sessionAuthenticationStrategy;
 		}
-		return new CsrfAuthenticationStrategy(this.csrfTokenRepository);
+		CsrfAuthenticationStrategy csrfAuthenticationStrategy = new CsrfAuthenticationStrategy(
+				this.csrfTokenRepository);
+		if (this.requestAttributeHandler != null) {
+			csrfAuthenticationStrategy.setRequestAttributeHandler(this.requestAttributeHandler);
+		}
+		return csrfAuthenticationStrategy;
 	}
 
 	/**

+ 14 - 6
config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java

@@ -67,13 +67,13 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
 
 	private static final String DISPATCHER_SERVLET_CLASS_NAME = "org.springframework.web.servlet.DispatcherServlet";
 
-	private static final String ATT_REQUEST_ATTRIBUTE_NAME = "request-attribute-name";
-
 	private static final String ATT_MATCHER = "request-matcher-ref";
 
 	private static final String ATT_REPOSITORY = "token-repository-ref";
 
-	private String requestAttributeName;
+	private static final String ATT_REQUEST_ATTRIBUTE_HANDLER = "request-attribute-handler-ref";
+
+	private static final String ATT_REQUEST_RESOLVER = "request-resolver-ref";
 
 	private String csrfRepositoryRef;
 
@@ -81,6 +81,10 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
 
 	private String requestMatcherRef;
 
+	private String requestAttributeHandlerRef;
+
+	private String requestResolverRef;
+
 	@Override
 	public BeanDefinition parse(Element element, ParserContext pc) {
 		boolean disabled = element != null && "true".equals(element.getAttribute("disabled"));
@@ -98,8 +102,9 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
 		}
 		if (element != null) {
 			this.csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY);
-			this.requestAttributeName = element.getAttribute(ATT_REQUEST_ATTRIBUTE_NAME);
 			this.requestMatcherRef = element.getAttribute(ATT_MATCHER);
+			this.requestAttributeHandlerRef = element.getAttribute(ATT_REQUEST_ATTRIBUTE_HANDLER);
+			this.requestResolverRef = element.getAttribute(ATT_REQUEST_RESOLVER);
 		}
 		if (!StringUtils.hasText(this.csrfRepositoryRef)) {
 			RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class);
@@ -115,8 +120,11 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
 		if (StringUtils.hasText(this.requestMatcherRef)) {
 			builder.addPropertyReference("requireCsrfProtectionMatcher", this.requestMatcherRef);
 		}
-		if (StringUtils.hasText(this.requestAttributeName)) {
-			builder.addPropertyValue("csrfRequestAttributeName", this.requestAttributeName);
+		if (StringUtils.hasText(this.requestAttributeHandlerRef)) {
+			builder.addPropertyReference("requestAttributeHandler", this.requestAttributeHandlerRef);
+		}
+		if (StringUtils.hasText(this.requestResolverRef)) {
+			builder.addPropertyReference("requestResolver", this.requestResolverRef);
 		}
 		this.csrfFilter = builder.getBeanDefinition();
 		return this.csrfFilter;

+ 6 - 3
config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc

@@ -1142,15 +1142,18 @@ csrf =
 csrf-options.attlist &=
 	## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled).
 	attribute disabled {xsd:boolean}?
-csrf-options.attlist &=
-	## The request attribute name the CsrfToken is set on. Default is to set to CsrfToken.parameterName
-	attribute request-attribute-name { xsd:token }?
 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 wrapped by LazyCsrfTokenRepository.
 	attribute token-repository-ref { xsd:token }?
+csrf-options.attlist &=
+	## The CsrfTokenRequestAttributeHandler to use. The default is CsrfTokenRequestProcessor.
+	attribute request-attribute-handler-ref { xsd:token }?
+csrf-options.attlist &=
+	## The CsrfTokenRequestResolver to use. The default is CsrfTokenRequestProcessor.
+	attribute request-resolver-ref { xsd:token }?
 
 headers =
 ## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.

+ 12 - 7
config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd

@@ -3235,13 +3235,6 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
-      <xs:attribute name="request-attribute-name" type="xs:token">
-         <xs:annotation>
-            <xs:documentation>The request attribute name the CsrfToken is set on. Default is to set to
-                CsrfToken.parameterName
-                </xs:documentation>
-         </xs:annotation>
-      </xs:attribute>
       <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
@@ -3256,6 +3249,18 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
+      <xs:attribute name="request-attribute-handler-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The CsrfTokenRequestAttributeHandler to use. The default is CsrfTokenRequestProcessor.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="request-resolver-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The CsrfTokenRequestResolver to use. The default is CsrfTokenRequestProcessor.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
   </xs:attributeGroup>
   <xs:element name="headers">
       <xs:annotation>

+ 4 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configuration/DeferHttpSessionJavaConfigTests.java

@@ -33,6 +33,7 @@ import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.web.DefaultSecurityFilterChain;
 import org.springframework.security.web.FilterChainProxy;
+import org.springframework.security.web.csrf.CsrfTokenRequestProcessor;
 import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
 import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
 import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
@@ -84,6 +85,8 @@ public class DeferHttpSessionJavaConfigTests {
 			csrfRepository.setDeferLoadToken(true);
 			HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
 			requestCache.setMatchingRequestParameterName("continue");
+			CsrfTokenRequestProcessor requestAttributeHandler = new CsrfTokenRequestProcessor();
+			requestAttributeHandler.setCsrfRequestAttributeName("_csrf");
 			// @formatter:off
 			http
 				.requestCache((cache) -> cache
@@ -99,7 +102,7 @@ public class DeferHttpSessionJavaConfigTests {
 					.requireExplicitAuthenticationStrategy(true)
 				)
 				.csrf((csrf) -> csrf
-					.csrfRequestAttributeName("_csrf")
+					.csrfTokenRequestAttributeHandler(requestAttributeHandler)
 					.csrfTokenRepository(csrfRepository)
 				);
 			// @formatter:on

+ 89 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -30,6 +30,7 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
 import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.config.Customizer;
 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;
@@ -38,9 +39,12 @@ import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.userdetails.PasswordEncodedUser;
+import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.web.csrf.CsrfToken;
 import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.csrf.CsrfTokenRequestProcessor;
 import org.springframework.security.web.csrf.DefaultCsrfToken;
 import org.springframework.security.web.firewall.StrictHttpFirewall;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -55,12 +59,16 @@ import org.springframework.web.servlet.support.RequestDataValueProcessor;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.hamcrest.Matchers.containsString;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.springframework.security.config.Customizer.withDefaults;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -74,6 +82,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@@ -84,6 +93,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
  * @author Eleftheria Stein
  * @author Michael Vitz
  * @author Sam Simmons
+ * @author Steve Riesenberg
  */
 @ExtendWith(SpringTestContextExtension.class)
 public class CsrfConfigurerTests {
@@ -407,6 +417,47 @@ public class CsrfConfigurerTests {
 				any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 
+	@Test
+	public void getLoginWhenCsrfTokenRequestProcessorSetThenRespondsWithNormalCsrfToken() throws Exception {
+		CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
+		CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
+		given(csrfTokenRepository.generateToken(any(HttpServletRequest.class))).willReturn(csrfToken);
+		CsrfTokenRequestProcessorConfig.REPO = csrfTokenRepository;
+		CsrfTokenRequestProcessorConfig.PROCESSOR = new CsrfTokenRequestProcessor();
+		this.spring.register(CsrfTokenRequestProcessorConfig.class, BasicController.class).autowire();
+		this.mvc.perform(get("/login")).andExpect(status().isOk())
+				.andExpect(content().string(containsString(csrfToken.getToken())));
+		verify(csrfTokenRepository).loadToken(any(HttpServletRequest.class));
+		verify(csrfTokenRepository).generateToken(any(HttpServletRequest.class));
+		verify(csrfTokenRepository).saveToken(eq(csrfToken), any(HttpServletRequest.class),
+				any(HttpServletResponse.class));
+		verifyNoMoreInteractions(csrfTokenRepository);
+	}
+
+	@Test
+	public void loginWhenCsrfTokenRequestProcessorSetAndNormalCsrfTokenThenSuccess() throws Exception {
+		CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
+		CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
+		given(csrfTokenRepository.loadToken(any(HttpServletRequest.class))).willReturn(csrfToken);
+		given(csrfTokenRepository.generateToken(any(HttpServletRequest.class))).willReturn(csrfToken);
+		CsrfTokenRequestProcessorConfig.REPO = csrfTokenRepository;
+		CsrfTokenRequestProcessorConfig.PROCESSOR = new CsrfTokenRequestProcessor();
+		this.spring.register(CsrfTokenRequestProcessorConfig.class, BasicController.class).autowire();
+		// @formatter:off
+		MockHttpServletRequestBuilder loginRequest = post("/login")
+				.header(csrfToken.getHeaderName(), csrfToken.getToken())
+				.param("username", "user")
+				.param("password", "password");
+		// @formatter:on
+		this.mvc.perform(loginRequest).andExpect(redirectedUrl("/"));
+		verify(csrfTokenRepository, times(2)).loadToken(any(HttpServletRequest.class));
+		verify(csrfTokenRepository).saveToken(isNull(), any(HttpServletRequest.class), any(HttpServletResponse.class));
+		verify(csrfTokenRepository).generateToken(any(HttpServletRequest.class));
+		verify(csrfTokenRepository).saveToken(eq(csrfToken), any(HttpServletRequest.class),
+				any(HttpServletResponse.class));
+		verifyNoMoreInteractions(csrfTokenRepository);
+	}
+
 	@Configuration
 	static class AllowHttpMethodsFirewallConfig {
 
@@ -748,6 +799,43 @@ public class CsrfConfigurerTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class CsrfTokenRequestProcessorConfig {
+
+		static CsrfTokenRepository REPO;
+
+		static CsrfTokenRequestProcessor PROCESSOR;
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize
+					.anyRequest().authenticated()
+				)
+				.formLogin(Customizer.withDefaults())
+				.csrf((csrf) -> csrf
+					.csrfTokenRepository(REPO)
+					.csrfTokenRequestAttributeHandler(PROCESSOR)
+					.csrfTokenRequestResolver(PROCESSOR)
+				);
+			// @formatter:on
+
+			return http.build();
+		}
+
+		@Autowired
+		void configure(AuthenticationManagerBuilder auth) throws Exception {
+			// @formatter:off
+			auth
+					.inMemoryAuthentication()
+					.withUser(PasswordEncodedUser.user());
+			// @formatter:on
+		}
+
+	}
+
 	@RestController
 	static class BasicController {
 

+ 1 - 1
config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2022 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.

+ 4 - 1
config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestAttrName.xml

@@ -16,14 +16,17 @@
   -->
 
 <b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		 xmlns:p="http://www.springframework.org/schema/p"
 		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 		 xmlns="http://www.springframework.org/schema/security"
 		 xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
 		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
 
 	<http auto-config="true">
-		<csrf request-attribute-name="csrf-attribute-name"/>
+		<csrf request-attribute-handler-ref="requestAttributeHandler"/>
 	</http>
 
+	<b:bean id="requestAttributeHandler" class="org.springframework.security.web.csrf.CsrfTokenRequestProcessor"
+		p:csrfRequestAttributeName="csrf-attribute-name"/>
 	<b:import resource="CsrfConfigTests-shared-userservice.xml"/>
 </b:beans>

+ 3 - 1
config/src/test/resources/org/springframework/security/config/http/DeferHttpSessionTests-Explicit.xml

@@ -30,7 +30,7 @@
 			security-context-explicit-save="true"
 			use-authorization-manager="true">
 		<intercept-url  pattern="/**" access="permitAll"/>
-		<csrf request-attribute-name="_csrf"
+		<csrf request-attribute-handler-ref="requestAttributeHandler"
 			token-repository-ref="csrfRepository"/>
 		<request-cache ref="requestCache"/>
 		<session-management authentication-strategy-explicit-invocation="true"/>
@@ -42,5 +42,7 @@
 	<b:bean id="csrfRepository" class="org.springframework.security.web.csrf.LazyCsrfTokenRepository"
 		c:delegate-ref="httpSessionCsrfRepository"
 	 	p:deferLoadToken="true"/>
+	<b:bean id="requestAttributeHandler" class="org.springframework.security.web.csrf.CsrfTokenRequestProcessor"
+		p:csrfRequestAttributeName="_csrf"/>
 	<b:import resource="CsrfConfigTests-shared-userservice.xml"/>
 </b:beans>

+ 7 - 4
docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc

@@ -775,10 +775,13 @@ It is highly recommended to leave CSRF protection enabled.
 The CsrfTokenRepository to use.
 The default is `HttpSessionCsrfTokenRepository`.
 
-[[nsa-csrf-request-attribute-name]]
-* **request-attribute-name**
-Optional attribute that specifies the request attribute name to set the `CsrfToken` on.
-The default is `CsrfToken.parameterName`.
+[[nsa-csrf-request-attribute-handler-ref]]
+* **request-attribute-handler-ref**
+The optional `CsrfTokenRequestAttributeHandler` to use. The default is `CsrfTokenRequestProcessor`.
+
+[[nsa-csrf-request-resolver-ref]]
+* **request-resolver-ref**
+The optional `CsrfTokenRequestResolver` to use. The default is `CsrfTokenRequestProcessor`.
 
 [[nsa-csrf-request-matcher-ref]]
 * **request-matcher-ref**

+ 14 - 3
web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -41,6 +41,8 @@ public final class CsrfAuthenticationStrategy implements SessionAuthenticationSt
 
 	private final CsrfTokenRepository csrfTokenRepository;
 
+	private CsrfTokenRequestAttributeHandler requestAttributeHandler = new CsrfTokenRequestProcessor();
+
 	/**
 	 * Creates a new instance
 	 * @param csrfTokenRepository the {@link CsrfTokenRepository} to use
@@ -50,6 +52,16 @@ public final class CsrfAuthenticationStrategy implements SessionAuthenticationSt
 		this.csrfTokenRepository = csrfTokenRepository;
 	}
 
+	/**
+	 * Specify a {@link CsrfTokenRequestAttributeHandler} to use for making the
+	 * {@code CsrfToken} available as a request attribute.
+	 * @param requestAttributeHandler the {@link CsrfTokenRequestAttributeHandler} to use
+	 */
+	public void setRequestAttributeHandler(CsrfTokenRequestAttributeHandler requestAttributeHandler) {
+		Assert.notNull(requestAttributeHandler, "requestAttributeHandler cannot be null");
+		this.requestAttributeHandler = requestAttributeHandler;
+	}
+
 	@Override
 	public void onAuthentication(Authentication authentication, HttpServletRequest request,
 			HttpServletResponse response) throws SessionAuthenticationException {
@@ -58,8 +70,7 @@ public final class CsrfAuthenticationStrategy implements SessionAuthenticationSt
 			this.csrfTokenRepository.saveToken(null, request, response);
 			CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
 			this.csrfTokenRepository.saveToken(newToken, request, response);
-			request.setAttribute(CsrfToken.class.getName(), newToken);
-			request.setAttribute(newToken.getParameterName(), newToken);
+			this.requestAttributeHandler.handle(request, response, () -> newToken);
 			this.logger.debug("Replaced CSRF Token");
 		}
 	}

+ 37 - 18
web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -58,6 +58,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
  * </p>
  *
  * @author Rob Winch
+ * @author Steve Riesenberg
  * @since 3.2
  */
 public final class CsrfFilter extends OncePerRequestFilter {
@@ -87,11 +88,16 @@ public final class CsrfFilter extends OncePerRequestFilter {
 
 	private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
 
-	private String csrfRequestAttributeName;
+	private CsrfTokenRequestAttributeHandler requestAttributeHandler;
+
+	private CsrfTokenRequestResolver requestResolver;
 
 	public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
 		Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
 		this.tokenRepository = csrfTokenRepository;
+		CsrfTokenRequestProcessor csrfTokenRequestProcessor = new CsrfTokenRequestProcessor();
+		this.requestAttributeHandler = csrfTokenRequestProcessor;
+		this.requestResolver = csrfTokenRequestProcessor;
 	}
 
 	@Override
@@ -109,10 +115,8 @@ public final class CsrfFilter extends OncePerRequestFilter {
 			csrfToken = this.tokenRepository.generateToken(request);
 			this.tokenRepository.saveToken(csrfToken, request, response);
 		}
-		request.setAttribute(CsrfToken.class.getName(), csrfToken);
-		String csrfAttrName = (this.csrfRequestAttributeName != null) ? this.csrfRequestAttributeName
-				: csrfToken.getParameterName();
-		request.setAttribute(csrfAttrName, csrfToken);
+		final CsrfToken finalCsrfToken = csrfToken;
+		this.requestAttributeHandler.handle(request, response, () -> finalCsrfToken);
 		if (!this.requireCsrfProtectionMatcher.matches(request)) {
 			if (this.logger.isTraceEnabled()) {
 				this.logger.trace("Did not protect against CSRF since request did not match "
@@ -121,10 +125,7 @@ public final class CsrfFilter extends OncePerRequestFilter {
 			filterChain.doFilter(request, response);
 			return;
 		}
-		String actualToken = request.getHeader(csrfToken.getHeaderName());
-		if (actualToken == null) {
-			actualToken = request.getParameter(csrfToken.getParameterName());
-		}
+		String actualToken = this.requestResolver.resolveCsrfTokenValue(request, csrfToken);
 		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
 			this.logger.debug(
 					LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
@@ -172,15 +173,33 @@ public final class CsrfFilter extends OncePerRequestFilter {
 	}
 
 	/**
-	 * The {@link CsrfToken} is available as a request attribute named
-	 * {@code CsrfToken.class.getName()}. By default, an additional request attribute that
-	 * is the same as {@link CsrfToken#getParameterName()} is set. This attribute allows
-	 * overriding the additional attribute.
-	 * @param csrfRequestAttributeName the name of an additional request attribute with
-	 * the value of the CsrfToken. Default is {@link CsrfToken#getParameterName()}
+	 * Specifies a {@link CsrfTokenRequestAttributeHandler} that is used to make the
+	 * {@link CsrfToken} available as a request attribute.
+	 *
+	 * <p>
+	 * The default is {@link CsrfTokenRequestProcessor}.
+	 * </p>
+	 * @param requestAttributeHandler the {@link CsrfTokenRequestAttributeHandler} to use
+	 * @since 5.8
+	 */
+	public void setRequestAttributeHandler(CsrfTokenRequestAttributeHandler requestAttributeHandler) {
+		Assert.notNull(requestAttributeHandler, "requestAttributeHandler cannot be null");
+		this.requestAttributeHandler = requestAttributeHandler;
+	}
+
+	/**
+	 * Specifies a {@link CsrfTokenRequestResolver} that is used to resolve the token
+	 * value from the request.
+	 *
+	 * <p>
+	 * The default is {@link CsrfTokenRequestProcessor}.
+	 * </p>
+	 * @param requestResolver the {@link CsrfTokenRequestResolver} to use
+	 * @since 5.8
 	 */
-	public void setCsrfRequestAttributeName(String csrfRequestAttributeName) {
-		this.csrfRequestAttributeName = csrfRequestAttributeName;
+	public void setRequestResolver(CsrfTokenRequestResolver requestResolver) {
+		Assert.notNull(requestResolver, "requestResolver cannot be null");
+		this.requestResolver = requestResolver;
 	}
 
 	/**

+ 45 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestAttributeHandler.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2002-2022 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
+ *
+ *      https://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.function.Supplier;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A callback interface that is used to make the {@link CsrfToken} created by the
+ * {@link CsrfTokenRepository} available as a request attribute. Implementations of this
+ * interface may choose to perform additional tasks or customize how the token is made
+ * available to the application through request attributes.
+ *
+ * @author Steve Riesenberg
+ * @since 5.8
+ * @see CsrfTokenRequestProcessor
+ */
+@FunctionalInterface
+public interface CsrfTokenRequestAttributeHandler {
+
+	/**
+	 * Handles a request using a {@link CsrfToken}.
+	 * @param request the {@code HttpServletRequest} being handled
+	 * @param response the {@code HttpServletResponse} being handled
+	 * @param csrfToken the {@link CsrfToken} created by the {@link CsrfTokenRepository}
+	 */
+	void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken);
+
+}

+ 75 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessor.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright 2002-2022 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
+ *
+ *      https://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.function.Supplier;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.util.Assert;
+
+/**
+ * An implementation of the {@link CsrfTokenRequestAttributeHandler} and
+ * {@link CsrfTokenRequestResolver} interfaces that is capable of making the
+ * {@link CsrfToken} available as a request attribute and resolving the token value as
+ * either a header or parameter value of the request.
+ *
+ * @author Steve Riesenberg
+ * @since 5.8
+ */
+public class CsrfTokenRequestProcessor implements CsrfTokenRequestAttributeHandler, CsrfTokenRequestResolver {
+
+	private String csrfRequestAttributeName;
+
+	/**
+	 * The {@link CsrfToken} is available as a request attribute named
+	 * {@code CsrfToken.class.getName()}. By default, an additional request attribute that
+	 * is the same as {@link CsrfToken#getParameterName()} is set. This attribute allows
+	 * overriding the additional attribute.
+	 * @param csrfRequestAttributeName the name of an additional request attribute with
+	 * the value of the CsrfToken. Default is {@link CsrfToken#getParameterName()}
+	 */
+	public final void setCsrfRequestAttributeName(String csrfRequestAttributeName) {
+		this.csrfRequestAttributeName = csrfRequestAttributeName;
+	}
+
+	@Override
+	public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
+		Assert.notNull(request, "request cannot be null");
+		Assert.notNull(response, "response cannot be null");
+		Assert.notNull(csrfToken, "csrfToken supplier cannot be null");
+		CsrfToken actualCsrfToken = csrfToken.get();
+		Assert.notNull(actualCsrfToken, "csrfToken cannot be null");
+		request.setAttribute(CsrfToken.class.getName(), actualCsrfToken);
+		String csrfAttrName = (this.csrfRequestAttributeName != null) ? this.csrfRequestAttributeName
+				: actualCsrfToken.getParameterName();
+		request.setAttribute(csrfAttrName, actualCsrfToken);
+	}
+
+	@Override
+	public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
+		Assert.notNull(request, "request cannot be null");
+		Assert.notNull(csrfToken, "csrfToken cannot be null");
+		String actualToken = request.getHeader(csrfToken.getHeaderName());
+		if (actualToken == null) {
+			actualToken = request.getParameter(csrfToken.getParameterName());
+		}
+		return actualToken;
+	}
+
+}

+ 42 - 0
web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestResolver.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2022 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
+ *
+ *      https://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;
+
+/**
+ * Implementations of this interface are capable of resolving the token value of a
+ * {@link CsrfToken} from the provided {@code HttpServletRequest}. Used by the
+ * {@link CsrfFilter}.
+ *
+ * @author Steve Riesenberg
+ * @since 5.8
+ * @see CsrfTokenRequestProcessor
+ */
+@FunctionalInterface
+public interface CsrfTokenRequestResolver {
+
+	/**
+	 * Returns the token value resolved from the provided {@code HttpServletRequest} and
+	 * {@link CsrfToken} or {@code null} if not available.
+	 * @param request the {@code HttpServletRequest} being processed
+	 * @param csrfToken the {@link CsrfToken} created by the {@link CsrfTokenRepository}
+	 * @return the token value resolved from the request
+	 */
+	String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken);
+
+}

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

@@ -34,8 +34,10 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 /**
  * @author Rob Winch
@@ -72,6 +74,25 @@ public class CsrfAuthenticationStrategyTests {
 		assertThatIllegalArgumentException().isThrownBy(() -> new CsrfAuthenticationStrategy(null));
 	}
 
+	@Test
+	public void setRequestAttributeHandlerWhenNullThenIllegalStateException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.strategy.setRequestAttributeHandler(null))
+				.withMessage("requestAttributeHandler cannot be null");
+	}
+
+	@Test
+	public void onAuthenticationWhenCustomRequestAttributeHandlerThenUsed() {
+		given(this.csrfTokenRepository.loadToken(this.request)).willReturn(this.existingToken);
+		given(this.csrfTokenRepository.generateToken(this.request)).willReturn(this.generatedToken);
+
+		CsrfTokenRequestAttributeHandler requestAttributeHandler = mock(CsrfTokenRequestAttributeHandler.class);
+		this.strategy.setRequestAttributeHandler(requestAttributeHandler);
+		this.strategy.onAuthentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"), this.request,
+				this.response);
+		verify(requestAttributeHandler).handle(eq(this.request), eq(this.response), any());
+		verifyNoMoreInteractions(requestAttributeHandler);
+	}
+
 	@Test
 	public void logoutRemovesCsrfTokenAndSavesNew() {
 		given(this.csrfTokenRepository.loadToken(this.request)).willReturn(this.existingToken);

+ 28 - 2
web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -335,6 +335,30 @@ public class CsrfFilterTests {
 		assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
 	}
 
+	@Test
+	public void doFilterWhenRequestAttributeHandlerThenUsed() throws Exception {
+		given(this.requestMatcher.matches(this.request)).willReturn(true);
+		given(this.tokenRepository.loadToken(this.request)).willReturn(this.token);
+		CsrfTokenRequestAttributeHandler requestAttributeHandler = mock(CsrfTokenRequestAttributeHandler.class);
+		this.filter.setRequestAttributeHandler(requestAttributeHandler);
+		this.request.setParameter(this.token.getParameterName(), this.token.getToken());
+		this.filter.doFilter(this.request, this.response, this.filterChain);
+		verify(requestAttributeHandler).handle(eq(this.request), eq(this.response), any());
+		verify(this.filterChain).doFilter(this.request, this.response);
+	}
+
+	@Test
+	public void doFilterWhenRequestResolverThenUsed() throws Exception {
+		given(this.requestMatcher.matches(this.request)).willReturn(true);
+		given(this.tokenRepository.loadToken(this.request)).willReturn(this.token);
+		CsrfTokenRequestResolver requestResolver = mock(CsrfTokenRequestResolver.class);
+		given(requestResolver.resolveCsrfTokenValue(this.request, this.token)).willReturn(this.token.getToken());
+		this.filter.setRequestResolver(requestResolver);
+		this.filter.doFilter(this.request, this.response, this.filterChain);
+		verify(requestResolver).resolveCsrfTokenValue(this.request, this.token);
+		verify(this.filterChain).doFilter(this.request, this.response);
+	}
+
 	@Test
 	public void setRequireCsrfProtectionMatcherNull() {
 		assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setRequireCsrfProtectionMatcher(null));
@@ -351,7 +375,9 @@ public class CsrfFilterTests {
 			throws ServletException, IOException {
 		CsrfFilter filter = createCsrfFilter(this.tokenRepository);
 		String csrfAttrName = "_csrf";
-		filter.setCsrfRequestAttributeName(csrfAttrName);
+		CsrfTokenRequestProcessor csrfTokenRequestProcessor = new CsrfTokenRequestProcessor();
+		csrfTokenRequestProcessor.setCsrfRequestAttributeName(csrfAttrName);
+		filter.setRequestAttributeHandler(csrfTokenRequestProcessor);
 		CsrfToken expectedCsrfToken = mock(CsrfToken.class);
 		given(this.tokenRepository.loadToken(this.request)).willReturn(expectedCsrfToken);
 

+ 134 - 0
web/src/test/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessorTests.java

@@ -0,0 +1,134 @@
+/*
+ * Copyright 2002-2022 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
+ *
+ *      https://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.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link CsrfTokenRequestProcessor}.
+ *
+ * @author Steve Riesenberg
+ * @since 5.8
+ */
+public class CsrfTokenRequestProcessorTests {
+
+	private MockHttpServletRequest request;
+
+	private MockHttpServletResponse response;
+
+	private CsrfToken token;
+
+	private CsrfTokenRequestProcessor processor;
+
+	@BeforeEach
+	public void setup() {
+		this.request = new MockHttpServletRequest();
+		this.response = new MockHttpServletResponse();
+		this.token = new DefaultCsrfToken("headerName", "paramName", "csrfTokenValue");
+		this.processor = new CsrfTokenRequestProcessor();
+	}
+
+	@Test
+	public void handleWhenRequestIsNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> this.processor.handle(null, this.response, () -> this.token))
+				.withMessage("request cannot be null");
+	}
+
+	@Test
+	public void handleWhenResponseIsNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> this.processor.handle(this.request, null, () -> this.token))
+				.withMessage("response cannot be null");
+	}
+
+	@Test
+	public void handleWhenCsrfTokenSupplierIsNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.processor.handle(this.request, this.response, null))
+				.withMessage("csrfToken supplier cannot be null");
+	}
+
+	@Test
+	public void handleWhenCsrfTokenIsNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> this.processor.handle(this.request, this.response, () -> null))
+				.withMessage("csrfToken cannot be null");
+	}
+
+	@Test
+	public void handleWhenCsrfRequestAttributeSetThenUsed() {
+		this.processor.setCsrfRequestAttributeName("_csrf");
+		this.processor.handle(this.request, this.response, () -> this.token);
+		assertThat(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
+		assertThat(this.request.getAttribute("_csrf")).isEqualTo(this.token);
+	}
+
+	@Test
+	public void handleWhenValidParametersThenRequestAttributesSet() {
+		this.processor.handle(this.request, this.response, () -> this.token);
+		assertThat(this.request.getAttribute(CsrfToken.class.getName())).isEqualTo(this.token);
+		assertThat(this.request.getAttribute(this.token.getParameterName())).isEqualTo(this.token);
+	}
+
+	@Test
+	public void resolveCsrfTokenValueWhenRequestIsNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.processor.resolveCsrfTokenValue(null, this.token))
+				.withMessage("request cannot be null");
+	}
+
+	@Test
+	public void resolveCsrfTokenValueWhenCsrfTokenIsNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.processor.resolveCsrfTokenValue(this.request, null))
+				.withMessage("csrfToken cannot be null");
+	}
+
+	@Test
+	public void resolveCsrfTokenValueWhenTokenNotSetThenReturnsNull() {
+		String tokenValue = this.processor.resolveCsrfTokenValue(this.request, this.token);
+		assertThat(tokenValue).isNull();
+	}
+
+	@Test
+	public void resolveCsrfTokenValueWhenParameterSetThenReturnsTokenValue() {
+		this.request.setParameter(this.token.getParameterName(), this.token.getToken());
+		String tokenValue = this.processor.resolveCsrfTokenValue(this.request, this.token);
+		assertThat(tokenValue).isEqualTo(this.token.getToken());
+	}
+
+	@Test
+	public void resolveCsrfTokenValueWhenHeaderSetThenReturnsTokenValue() {
+		this.request.addHeader(this.token.getHeaderName(), this.token.getToken());
+		String tokenValue = this.processor.resolveCsrfTokenValue(this.request, this.token);
+		assertThat(tokenValue).isEqualTo(this.token.getToken());
+	}
+
+	@Test
+	public void resolveCsrfTokenValueWhenHeaderAndParameterSetThenHeaderIsPreferred() {
+		this.request.addHeader(this.token.getHeaderName(), "header");
+		this.request.setParameter(this.token.getParameterName(), "parameter");
+		String tokenValue = this.processor.resolveCsrfTokenValue(this.request, this.token);
+		assertThat(tokenValue).isEqualTo("header");
+	}
+
+}