From 186601eae9fe3a5a2fea69bc1e1fd3365decc90e Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 11 Aug 2022 16:29:56 -0500 Subject: [PATCH] Add new interfaces for CSRF request processing Issue gh-4001 Issue gh-11456 --- .../web/configurers/CsrfConfigurer.java | 47 ++++-- .../config/http/CsrfBeanDefinitionParser.java | 20 ++- .../security/config/spring-security-5.8.rnc | 9 +- .../security/config/spring-security-5.8.xsd | 19 ++- .../DeferHttpSessionJavaConfigTests.java | 5 +- .../web/configurers/CsrfConfigurerTests.java | 90 +++++++++++- .../security/config/http/CsrfConfigTests.java | 2 +- .../CsrfConfigTests-WithRequestAttrName.xml | 5 +- .../http/DeferHttpSessionTests-Explicit.xml | 4 +- .../servlet/appendix/namespace/http.adoc | 11 +- .../web/csrf/CsrfAuthenticationStrategy.java | 17 ++- .../security/web/csrf/CsrfFilter.java | 55 ++++--- .../CsrfTokenRequestAttributeHandler.java | 45 ++++++ .../web/csrf/CsrfTokenRequestProcessor.java | 75 ++++++++++ .../web/csrf/CsrfTokenRequestResolver.java | 42 ++++++ .../csrf/CsrfAuthenticationStrategyTests.java | 21 +++ .../security/web/csrf/CsrfFilterTests.java | 30 +++- .../csrf/CsrfTokenRequestProcessorTests.java | 134 ++++++++++++++++++ 18 files changed, 572 insertions(+), 59 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestAttributeHandler.java create mode 100644 web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessor.java create mode 100644 web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestResolver.java create mode 100644 web/src/test/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessorTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index 3831190e906..1b755e315de 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/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.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> private SessionAuthenticationStrategy sessionAuthenticationStrategy; - private String csrfRequestAttributeName; + private CsrfTokenRequestAttributeHandler requestAttributeHandler; + + private CsrfTokenRequestResolver requestResolver; private final ApplicationContext context; @@ -127,12 +131,25 @@ public CsrfConfigurer requireCsrfProtectionMatcher(RequestMatcher requireCsrf } /** - * 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 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 csrfRequestAttributeName(String csrfRequestAttributeName) { - this.csrfRequestAttributeName = csrfRequestAttributeName; + public CsrfConfigurer csrfTokenRequestResolver(CsrfTokenRequestResolver requestResolver) { + this.requestResolver = requestResolver; return this; } @@ -214,9 +231,6 @@ public CsrfConfigurer sessionAuthenticationStrategy( @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 void configure(H http) { 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 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy() { 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; } /** diff --git a/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java index 495a2ddd2ad..c76d4c4d11e 100644 --- a/config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java +++ b/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 BeanDefinition parse(Element element, ParserContext pc) { } 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 BeanDefinition parse(Element element, ParserContext pc) { 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; diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc index bc0c0557628..3f48f7bcd48 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc +++ b/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. diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd index 37bda48acdf..bef39a7c620 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd @@ -3235,13 +3235,6 @@ - - - The request attribute name the CsrfToken is set on. Default is to set to - CsrfToken.parameterName - - - The RequestMatcher instance to be used to determine if CSRF should be applied. Default is @@ -3256,6 +3249,18 @@ + + + The CsrfTokenRequestAttributeHandler to use. The default is CsrfTokenRequestProcessor. + + + + + + The CsrfTokenRequestResolver to use. The default is CsrfTokenRequestProcessor. + + + diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/DeferHttpSessionJavaConfigTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/DeferHttpSessionJavaConfigTests.java index 1f24cf24ed2..b6cf0d68b57 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/DeferHttpSessionJavaConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/DeferHttpSessionJavaConfigTests.java @@ -33,6 +33,7 @@ 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 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { 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 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { .requireExplicitAuthenticationStrategy(true) ) .csrf((csrf) -> csrf - .csrfRequestAttributeName("_csrf") + .csrfTokenRequestAttributeHandler(requestAttributeHandler) .csrfTokenRepository(csrfRepository) ); // @formatter:on diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java index bc431b6afbb..3145696f727 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java +++ b/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.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.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 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.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 @@ * @author Eleftheria Stein * @author Michael Vitz * @author Sam Simmons + * @author Steve Riesenberg */ @ExtendWith(SpringTestContextExtension.class) public class CsrfConfigurerTests { @@ -407,6 +417,47 @@ public void csrfAuthenticationStrategyConfiguredThenStrategyUsed() throws Except 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 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } + @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 { diff --git a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java b/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java index a62bfae26dd..e9220895fbb 100644 --- a/config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java +++ b/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. diff --git a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestAttrName.xml b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestAttrName.xml index 4f6c27248ba..541f66453fe 100644 --- a/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestAttrName.xml +++ b/config/src/test/resources/org/springframework/security/config/http/CsrfConfigTests-WithRequestAttrName.xml @@ -16,14 +16,17 @@ --> - + + diff --git a/config/src/test/resources/org/springframework/security/config/http/DeferHttpSessionTests-Explicit.xml b/config/src/test/resources/org/springframework/security/config/http/DeferHttpSessionTests-Explicit.xml index 9be33976baa..2efb29d03ec 100644 --- a/config/src/test/resources/org/springframework/security/config/http/DeferHttpSessionTests-Explicit.xml +++ b/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"> - @@ -42,5 +42,7 @@ + diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 0253607b313..93e9addc4d7 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/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** diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java index f6a19a266d7..b61a20d7d5e 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java +++ b/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 CsrfAuthenticationStrategy(CsrfTokenRepository csrfTokenRepository) { 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 void onAuthentication(Authentication authentication, HttpServletRequest r 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"); } } diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java index 1c452ecf457..0033bf571ec 100644 --- a/web/src/main/java/org/springframework/security/web/csrf/CsrfFilter.java +++ b/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 @@ *

* * @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 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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 void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { } /** - * 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. + * + *

+ * The default is {@link CsrfTokenRequestProcessor}. + *

+ * @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. + * + *

+ * The default is {@link CsrfTokenRequestProcessor}. + *

+ * @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; } /** diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestAttributeHandler.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestAttributeHandler.java new file mode 100644 index 00000000000..a22f3144d22 --- /dev/null +++ b/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); + +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessor.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessor.java new file mode 100644 index 00000000000..47807a1dc1e --- /dev/null +++ b/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) { + 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; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestResolver.java b/web/src/main/java/org/springframework/security/web/csrf/CsrfTokenRequestResolver.java new file mode 100644 index 00000000000..f3d820d2ca7 --- /dev/null +++ b/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); + +} diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java index a949e3a3958..9872522aa0e 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java +++ b/web/src/test/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategyTests.java @@ -34,8 +34,10 @@ 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 void constructorNullCsrfTokenRepository() { 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); diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java index f107df43b0e..443375c35a8 100644 --- a/web/src/test/java/org/springframework/security/web/csrf/CsrfFilterTests.java +++ b/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 void doFilterWhenTokenIsNullThenNoNullPointer() throws Exception { 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 void doFilterWhenCsrfRequestAttributeNameThenNoCsrfTokenMethodInvokedOnGe 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); diff --git a/web/src/test/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessorTests.java b/web/src/test/java/org/springframework/security/web/csrf/CsrfTokenRequestProcessorTests.java new file mode 100644 index 00000000000..ac50ec3aaa8 --- /dev/null +++ b/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"); + } + +}