Skip to content

Commit 86fbb8d

Browse files
Steve Riesenbergsjohnr
Steve Riesenberg
authored andcommitted
Add new interfaces for CSRF request processing
Issue gh-4001 Issue gh-11456
1 parent ff6fd78 commit 86fbb8d

File tree

18 files changed

+572
-59
lines changed

18 files changed

+572
-59
lines changed

Diff for: config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

+36-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,6 +36,8 @@
3636
import org.springframework.security.web.csrf.CsrfFilter;
3737
import org.springframework.security.web.csrf.CsrfLogoutHandler;
3838
import org.springframework.security.web.csrf.CsrfTokenRepository;
39+
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
40+
import org.springframework.security.web.csrf.CsrfTokenRequestResolver;
3941
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
4042
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
4143
import org.springframework.security.web.csrf.MissingCsrfTokenException;
@@ -89,7 +91,9 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
8991

9092
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
9193

92-
private String csrfRequestAttributeName;
94+
private CsrfTokenRequestAttributeHandler requestAttributeHandler;
95+
96+
private CsrfTokenRequestResolver requestResolver;
9397

9498
private final ApplicationContext context;
9599

@@ -127,12 +131,25 @@ public CsrfConfigurer<H> requireCsrfProtectionMatcher(RequestMatcher requireCsrf
127131
}
128132

129133
/**
130-
* Sets the {@link CsrfFilter#setCsrfRequestAttributeName(String)}
131-
* @param csrfRequestAttributeName the attribute name to set the CsrfToken on.
132-
* @return the {@link CsrfConfigurer} for further customizations.
134+
* Specify a {@link CsrfTokenRequestAttributeHandler} to use for making the
135+
* {@code CsrfToken} available as a request attribute.
136+
* @param requestAttributeHandler the {@link CsrfTokenRequestAttributeHandler} to use
137+
* @return the {@link CsrfConfigurer} for further customizations
138+
*/
139+
public CsrfConfigurer<H> csrfTokenRequestAttributeHandler(
140+
CsrfTokenRequestAttributeHandler requestAttributeHandler) {
141+
this.requestAttributeHandler = requestAttributeHandler;
142+
return this;
143+
}
144+
145+
/**
146+
* Specify a {@link CsrfTokenRequestResolver} to use for resolving the token value
147+
* from the request.
148+
* @param requestResolver the {@link CsrfTokenRequestResolver} to use
149+
* @return the {@link CsrfConfigurer} for further customizations
133150
*/
134-
public CsrfConfigurer<H> csrfRequestAttributeName(String csrfRequestAttributeName) {
135-
this.csrfRequestAttributeName = csrfRequestAttributeName;
151+
public CsrfConfigurer<H> csrfTokenRequestResolver(CsrfTokenRequestResolver requestResolver) {
152+
this.requestResolver = requestResolver;
136153
return this;
137154
}
138155

@@ -214,9 +231,6 @@ public CsrfConfigurer<H> sessionAuthenticationStrategy(
214231
@Override
215232
public void configure(H http) {
216233
CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
217-
if (this.csrfRequestAttributeName != null) {
218-
filter.setCsrfRequestAttributeName(this.csrfRequestAttributeName);
219-
}
220234
RequestMatcher requireCsrfProtectionMatcher = getRequireCsrfProtectionMatcher();
221235
if (requireCsrfProtectionMatcher != null) {
222236
filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
@@ -233,6 +247,12 @@ public void configure(H http) {
233247
if (sessionConfigurer != null) {
234248
sessionConfigurer.addSessionAuthenticationStrategy(getSessionAuthenticationStrategy());
235249
}
250+
if (this.requestAttributeHandler != null) {
251+
filter.setRequestAttributeHandler(this.requestAttributeHandler);
252+
}
253+
if (this.requestResolver != null) {
254+
filter.setRequestResolver(this.requestResolver);
255+
}
236256
filter = postProcess(filter);
237257
http.addFilter(filter);
238258
}
@@ -321,7 +341,12 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy() {
321341
if (this.sessionAuthenticationStrategy != null) {
322342
return this.sessionAuthenticationStrategy;
323343
}
324-
return new CsrfAuthenticationStrategy(this.csrfTokenRepository);
344+
CsrfAuthenticationStrategy csrfAuthenticationStrategy = new CsrfAuthenticationStrategy(
345+
this.csrfTokenRepository);
346+
if (this.requestAttributeHandler != null) {
347+
csrfAuthenticationStrategy.setRequestAttributeHandler(this.requestAttributeHandler);
348+
}
349+
return csrfAuthenticationStrategy;
325350
}
326351

327352
/**

Diff for: config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java

+14-6
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,24 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
6767

6868
private static final String DISPATCHER_SERVLET_CLASS_NAME = "org.springframework.web.servlet.DispatcherServlet";
6969

70-
private static final String ATT_REQUEST_ATTRIBUTE_NAME = "request-attribute-name";
71-
7270
private static final String ATT_MATCHER = "request-matcher-ref";
7371

7472
private static final String ATT_REPOSITORY = "token-repository-ref";
7573

76-
private String requestAttributeName;
74+
private static final String ATT_REQUEST_ATTRIBUTE_HANDLER = "request-attribute-handler-ref";
75+
76+
private static final String ATT_REQUEST_RESOLVER = "request-resolver-ref";
7777

7878
private String csrfRepositoryRef;
7979

8080
private BeanDefinition csrfFilter;
8181

8282
private String requestMatcherRef;
8383

84+
private String requestAttributeHandlerRef;
85+
86+
private String requestResolverRef;
87+
8488
@Override
8589
public BeanDefinition parse(Element element, ParserContext pc) {
8690
boolean disabled = element != null && "true".equals(element.getAttribute("disabled"));
@@ -98,8 +102,9 @@ public BeanDefinition parse(Element element, ParserContext pc) {
98102
}
99103
if (element != null) {
100104
this.csrfRepositoryRef = element.getAttribute(ATT_REPOSITORY);
101-
this.requestAttributeName = element.getAttribute(ATT_REQUEST_ATTRIBUTE_NAME);
102105
this.requestMatcherRef = element.getAttribute(ATT_MATCHER);
106+
this.requestAttributeHandlerRef = element.getAttribute(ATT_REQUEST_ATTRIBUTE_HANDLER);
107+
this.requestResolverRef = element.getAttribute(ATT_REQUEST_RESOLVER);
103108
}
104109
if (!StringUtils.hasText(this.csrfRepositoryRef)) {
105110
RootBeanDefinition csrfTokenRepository = new RootBeanDefinition(HttpSessionCsrfTokenRepository.class);
@@ -115,8 +120,11 @@ public BeanDefinition parse(Element element, ParserContext pc) {
115120
if (StringUtils.hasText(this.requestMatcherRef)) {
116121
builder.addPropertyReference("requireCsrfProtectionMatcher", this.requestMatcherRef);
117122
}
118-
if (StringUtils.hasText(this.requestAttributeName)) {
119-
builder.addPropertyValue("csrfRequestAttributeName", this.requestAttributeName);
123+
if (StringUtils.hasText(this.requestAttributeHandlerRef)) {
124+
builder.addPropertyReference("requestAttributeHandler", this.requestAttributeHandlerRef);
125+
}
126+
if (StringUtils.hasText(this.requestResolverRef)) {
127+
builder.addPropertyReference("requestResolver", this.requestResolverRef);
120128
}
121129
this.csrfFilter = builder.getBeanDefinition();
122130
return this.csrfFilter;

Diff for: config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc

+6-3
Original file line numberDiff line numberDiff line change
@@ -1142,15 +1142,18 @@ csrf =
11421142
csrf-options.attlist &=
11431143
## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled).
11441144
attribute disabled {xsd:boolean}?
1145-
csrf-options.attlist &=
1146-
## The request attribute name the CsrfToken is set on. Default is to set to CsrfToken.parameterName
1147-
attribute request-attribute-name { xsd:token }?
11481145
csrf-options.attlist &=
11491146
## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS"
11501147
attribute request-matcher-ref { xsd:token }?
11511148
csrf-options.attlist &=
11521149
## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository.
11531150
attribute token-repository-ref { xsd:token }?
1151+
csrf-options.attlist &=
1152+
## The CsrfTokenRequestAttributeHandler to use. The default is CsrfTokenRequestProcessor.
1153+
attribute request-attribute-handler-ref { xsd:token }?
1154+
csrf-options.attlist &=
1155+
## The CsrfTokenRequestResolver to use. The default is CsrfTokenRequestProcessor.
1156+
attribute request-resolver-ref { xsd:token }?
11541157

11551158
headers =
11561159
## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.

Diff for: config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd

+12-7
Original file line numberDiff line numberDiff line change
@@ -3235,13 +3235,6 @@
32353235
</xs:documentation>
32363236
</xs:annotation>
32373237
</xs:attribute>
3238-
<xs:attribute name="request-attribute-name" type="xs:token">
3239-
<xs:annotation>
3240-
<xs:documentation>The request attribute name the CsrfToken is set on. Default is to set to
3241-
CsrfToken.parameterName
3242-
</xs:documentation>
3243-
</xs:annotation>
3244-
</xs:attribute>
32453238
<xs:attribute name="request-matcher-ref" type="xs:token">
32463239
<xs:annotation>
32473240
<xs:documentation>The RequestMatcher instance to be used to determine if CSRF should be applied. Default is
@@ -3256,6 +3249,18 @@
32563249
</xs:documentation>
32573250
</xs:annotation>
32583251
</xs:attribute>
3252+
<xs:attribute name="request-attribute-handler-ref" type="xs:token">
3253+
<xs:annotation>
3254+
<xs:documentation>The CsrfTokenRequestAttributeHandler to use. The default is CsrfTokenRequestProcessor.
3255+
</xs:documentation>
3256+
</xs:annotation>
3257+
</xs:attribute>
3258+
<xs:attribute name="request-resolver-ref" type="xs:token">
3259+
<xs:annotation>
3260+
<xs:documentation>The CsrfTokenRequestResolver to use. The default is CsrfTokenRequestProcessor.
3261+
</xs:documentation>
3262+
</xs:annotation>
3263+
</xs:attribute>
32593264
</xs:attributeGroup>
32603265
<xs:element name="headers">
32613266
<xs:annotation>

Diff for: config/src/test/java/org/springframework/security/config/annotation/web/configuration/DeferHttpSessionJavaConfigTests.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.security.config.test.SpringTestContextExtension;
3434
import org.springframework.security.web.DefaultSecurityFilterChain;
3535
import org.springframework.security.web.FilterChainProxy;
36+
import org.springframework.security.web.csrf.CsrfTokenRequestProcessor;
3637
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
3738
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
3839
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
@@ -84,6 +85,8 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
8485
csrfRepository.setDeferLoadToken(true);
8586
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
8687
requestCache.setMatchingRequestParameterName("continue");
88+
CsrfTokenRequestProcessor requestAttributeHandler = new CsrfTokenRequestProcessor();
89+
requestAttributeHandler.setCsrfRequestAttributeName("_csrf");
8790
// @formatter:off
8891
http
8992
.requestCache((cache) -> cache
@@ -99,7 +102,7 @@ DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
99102
.requireExplicitAuthenticationStrategy(true)
100103
)
101104
.csrf((csrf) -> csrf
102-
.csrfRequestAttributeName("_csrf")
105+
.csrfTokenRequestAttributeHandler(requestAttributeHandler)
103106
.csrfTokenRepository(csrfRepository)
104107
);
105108
// @formatter:on

Diff for: config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java

+89-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.springframework.context.annotation.Configuration;
3131
import org.springframework.http.HttpMethod;
3232
import org.springframework.mock.web.MockHttpSession;
33+
import org.springframework.security.config.Customizer;
3334
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
3435
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3536
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -38,9 +39,12 @@
3839
import org.springframework.security.config.test.SpringTestContextExtension;
3940
import org.springframework.security.core.Authentication;
4041
import org.springframework.security.core.userdetails.PasswordEncodedUser;
42+
import org.springframework.security.web.SecurityFilterChain;
4143
import org.springframework.security.web.access.AccessDeniedHandler;
4244
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
45+
import org.springframework.security.web.csrf.CsrfToken;
4346
import org.springframework.security.web.csrf.CsrfTokenRepository;
47+
import org.springframework.security.web.csrf.CsrfTokenRequestProcessor;
4448
import org.springframework.security.web.csrf.DefaultCsrfToken;
4549
import org.springframework.security.web.firewall.StrictHttpFirewall;
4650
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -55,12 +59,16 @@
5559

5660
import static org.assertj.core.api.Assertions.assertThat;
5761
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
62+
import static org.hamcrest.Matchers.containsString;
5863
import static org.mockito.ArgumentMatchers.any;
64+
import static org.mockito.ArgumentMatchers.eq;
5965
import static org.mockito.ArgumentMatchers.isNull;
6066
import static org.mockito.BDDMockito.given;
6167
import static org.mockito.Mockito.atLeastOnce;
6268
import static org.mockito.Mockito.mock;
69+
import static org.mockito.Mockito.times;
6370
import static org.mockito.Mockito.verify;
71+
import static org.mockito.Mockito.verifyNoMoreInteractions;
6472
import static org.springframework.security.config.Customizer.withDefaults;
6573
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
6674
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -74,6 +82,7 @@
7482
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
7583
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
7684
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
85+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
7786
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
7887
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
7988

@@ -84,6 +93,7 @@
8493
* @author Eleftheria Stein
8594
* @author Michael Vitz
8695
* @author Sam Simmons
96+
* @author Steve Riesenberg
8797
*/
8898
@ExtendWith(SpringTestContextExtension.class)
8999
public class CsrfConfigurerTests {
@@ -407,6 +417,47 @@ public void csrfAuthenticationStrategyConfiguredThenStrategyUsed() throws Except
407417
any(HttpServletRequest.class), any(HttpServletResponse.class));
408418
}
409419

420+
@Test
421+
public void getLoginWhenCsrfTokenRequestProcessorSetThenRespondsWithNormalCsrfToken() throws Exception {
422+
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
423+
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
424+
given(csrfTokenRepository.generateToken(any(HttpServletRequest.class))).willReturn(csrfToken);
425+
CsrfTokenRequestProcessorConfig.REPO = csrfTokenRepository;
426+
CsrfTokenRequestProcessorConfig.PROCESSOR = new CsrfTokenRequestProcessor();
427+
this.spring.register(CsrfTokenRequestProcessorConfig.class, BasicController.class).autowire();
428+
this.mvc.perform(get("/login")).andExpect(status().isOk())
429+
.andExpect(content().string(containsString(csrfToken.getToken())));
430+
verify(csrfTokenRepository).loadToken(any(HttpServletRequest.class));
431+
verify(csrfTokenRepository).generateToken(any(HttpServletRequest.class));
432+
verify(csrfTokenRepository).saveToken(eq(csrfToken), any(HttpServletRequest.class),
433+
any(HttpServletResponse.class));
434+
verifyNoMoreInteractions(csrfTokenRepository);
435+
}
436+
437+
@Test
438+
public void loginWhenCsrfTokenRequestProcessorSetAndNormalCsrfTokenThenSuccess() throws Exception {
439+
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token");
440+
CsrfTokenRepository csrfTokenRepository = mock(CsrfTokenRepository.class);
441+
given(csrfTokenRepository.loadToken(any(HttpServletRequest.class))).willReturn(csrfToken);
442+
given(csrfTokenRepository.generateToken(any(HttpServletRequest.class))).willReturn(csrfToken);
443+
CsrfTokenRequestProcessorConfig.REPO = csrfTokenRepository;
444+
CsrfTokenRequestProcessorConfig.PROCESSOR = new CsrfTokenRequestProcessor();
445+
this.spring.register(CsrfTokenRequestProcessorConfig.class, BasicController.class).autowire();
446+
// @formatter:off
447+
MockHttpServletRequestBuilder loginRequest = post("/login")
448+
.header(csrfToken.getHeaderName(), csrfToken.getToken())
449+
.param("username", "user")
450+
.param("password", "password");
451+
// @formatter:on
452+
this.mvc.perform(loginRequest).andExpect(redirectedUrl("/"));
453+
verify(csrfTokenRepository, times(2)).loadToken(any(HttpServletRequest.class));
454+
verify(csrfTokenRepository).saveToken(isNull(), any(HttpServletRequest.class), any(HttpServletResponse.class));
455+
verify(csrfTokenRepository).generateToken(any(HttpServletRequest.class));
456+
verify(csrfTokenRepository).saveToken(eq(csrfToken), any(HttpServletRequest.class),
457+
any(HttpServletResponse.class));
458+
verifyNoMoreInteractions(csrfTokenRepository);
459+
}
460+
410461
@Configuration
411462
static class AllowHttpMethodsFirewallConfig {
412463

@@ -748,6 +799,43 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
748799

749800
}
750801

802+
@Configuration
803+
@EnableWebSecurity
804+
static class CsrfTokenRequestProcessorConfig {
805+
806+
static CsrfTokenRepository REPO;
807+
808+
static CsrfTokenRequestProcessor PROCESSOR;
809+
810+
@Bean
811+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
812+
// @formatter:off
813+
http
814+
.authorizeHttpRequests((authorize) -> authorize
815+
.anyRequest().authenticated()
816+
)
817+
.formLogin(Customizer.withDefaults())
818+
.csrf((csrf) -> csrf
819+
.csrfTokenRepository(REPO)
820+
.csrfTokenRequestAttributeHandler(PROCESSOR)
821+
.csrfTokenRequestResolver(PROCESSOR)
822+
);
823+
// @formatter:on
824+
825+
return http.build();
826+
}
827+
828+
@Autowired
829+
void configure(AuthenticationManagerBuilder auth) throws Exception {
830+
// @formatter:off
831+
auth
832+
.inMemoryAuthentication()
833+
.withUser(PasswordEncodedUser.user());
834+
// @formatter:on
835+
}
836+
837+
}
838+
751839
@RestController
752840
static class BasicController {
753841

Diff for: config/src/test/java/org/springframework/security/config/http/CsrfConfigTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)