Create CsrfCustomizer for SPA configuration
Closes gh-14149 Signed-off-by: Felix Hagemans <felixhagemans@gmail.com>
This commit is contained in:
committed by
Josh Cummings
parent
52394c1f07
commit
1a4de49977
@@ -19,9 +19,11 @@ package org.springframework.security.config.annotation.web.configurers;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.micrometer.observation.ObservationRegistry;
|
import io.micrometer.observation.ObservationRegistry;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
@@ -34,13 +36,17 @@ import org.springframework.security.web.access.CompositeAccessDeniedHandler;
|
|||||||
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
|
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
|
||||||
import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler;
|
import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler;
|
||||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||||
|
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||||
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
|
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
|
||||||
import org.springframework.security.web.csrf.CsrfFilter;
|
import org.springframework.security.web.csrf.CsrfFilter;
|
||||||
import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
||||||
|
import org.springframework.security.web.csrf.CsrfToken;
|
||||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||||
|
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||||
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
||||||
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
||||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||||
|
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
|
||||||
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
|
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
|
||||||
import org.springframework.security.web.session.InvalidSessionStrategy;
|
import org.springframework.security.web.session.InvalidSessionStrategy;
|
||||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||||
@@ -48,6 +54,7 @@ import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
|
|||||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds
|
* Adds
|
||||||
@@ -214,6 +221,21 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Sensible CSRF defaults when used in combination with a single page application.
|
||||||
|
* Creates a cookie-based token repository and a custom request handler to resolve the
|
||||||
|
* actual token value instead of the encoded token.
|
||||||
|
* </p>
|
||||||
|
* @return the {@link CsrfConfigurer} for further customizations
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public CsrfConfigurer<H> spa() {
|
||||||
|
this.csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
|
this.requestHandler = new SpaCsrfTokenRequestHandler();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Override
|
@Override
|
||||||
public void configure(H http) {
|
public void configure(H http) {
|
||||||
@@ -375,4 +397,42 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
|
||||||
|
|
||||||
|
private final CsrfTokenRequestAttributeHandler plain = new CsrfTokenRequestAttributeHandler();
|
||||||
|
|
||||||
|
private final CsrfTokenRequestAttributeHandler xor = new XorCsrfTokenRequestAttributeHandler();
|
||||||
|
|
||||||
|
SpaCsrfTokenRequestHandler() {
|
||||||
|
this.xor.setCsrfRequestAttributeName(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
|
||||||
|
/*
|
||||||
|
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection
|
||||||
|
* of the CsrfToken when it is rendered in the response body.
|
||||||
|
*/
|
||||||
|
this.xor.handle(request, response, csrfToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
|
||||||
|
String headerValue = request.getHeader(csrfToken.getHeaderName());
|
||||||
|
/*
|
||||||
|
* If the request contains a request header, use
|
||||||
|
* CsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
|
||||||
|
* when a single-page application includes the header value automatically,
|
||||||
|
* which was obtained via a cookie containing the raw CsrfToken.
|
||||||
|
*
|
||||||
|
* In all other cases (e.g. if the request contains a request parameter), use
|
||||||
|
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
|
||||||
|
* when a server-side rendered form includes the _csrf request parameter as a
|
||||||
|
* hidden input.
|
||||||
|
*/
|
||||||
|
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
|||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
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.request.MockMvcRequestBuilders.request;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@@ -613,6 +614,37 @@ public class CsrfConfigurerTests {
|
|||||||
assertThat(cookies).isEmpty();
|
assertThat(cookies).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void spaConfigForbidden() throws Exception {
|
||||||
|
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
|
||||||
|
.autowire();
|
||||||
|
this.mvc.perform(post("/")).andExpect(status().isForbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void spaConfigOk() throws Exception {
|
||||||
|
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
|
||||||
|
.autowire();
|
||||||
|
this.mvc.perform(post("/").with(csrf())).andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void spaConfigDoubleSubmit() throws Exception {
|
||||||
|
this.spring.register(CsrfSpaConfig.class, AllowHttpMethodsFirewallConfig.class, BasicController.class)
|
||||||
|
.autowire();
|
||||||
|
var token = this.mvc.perform(post("/"))
|
||||||
|
.andExpect(status().isForbidden())
|
||||||
|
.andExpect(cookie().exists("XSRF-TOKEN"))
|
||||||
|
.andReturn()
|
||||||
|
.getResponse()
|
||||||
|
.getCookie("XSRF-TOKEN");
|
||||||
|
|
||||||
|
this.mvc
|
||||||
|
.perform(post("/").header("X-XSRF-TOKEN", token.getValue())
|
||||||
|
.cookie(new Cookie("XSRF-TOKEN", token.getValue())))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
static class AllowHttpMethodsFirewallConfig {
|
static class AllowHttpMethodsFirewallConfig {
|
||||||
|
|
||||||
@@ -1006,6 +1038,18 @@ public class CsrfConfigurerTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
static class CsrfSpaConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.csrf(CsrfConfigurer::spa);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
static class HttpBasicCsrfTokenRequestHandlerConfig {
|
static class HttpBasicCsrfTokenRequestHandlerConfig {
|
||||||
|
|||||||
@@ -787,48 +787,10 @@ public class SecurityConfig {
|
|||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
// ...
|
// ...
|
||||||
.csrf((csrf) -> csrf
|
.csrf((csrf) -> csrf.spa());
|
||||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1>
|
|
||||||
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2>
|
|
||||||
);
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
|
|
||||||
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
|
|
||||||
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
|
|
||||||
/*
|
|
||||||
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
|
|
||||||
* the CsrfToken when it is rendered in the response body.
|
|
||||||
*/
|
|
||||||
this.xor.handle(request, response, csrfToken);
|
|
||||||
/*
|
|
||||||
* Render the token value to a cookie by causing the deferred token to be loaded.
|
|
||||||
*/
|
|
||||||
csrfToken.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
|
|
||||||
String headerValue = request.getHeader(csrfToken.getHeaderName());
|
|
||||||
/*
|
|
||||||
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
|
|
||||||
* to resolve the CsrfToken. This applies when a single-page application includes
|
|
||||||
* the header value automatically, which was obtained via a cookie containing the
|
|
||||||
* raw CsrfToken.
|
|
||||||
*
|
|
||||||
* In all other cases (e.g. if the request contains a request parameter), use
|
|
||||||
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
|
|
||||||
* when a server-side rendered form includes the _csrf request parameter as a
|
|
||||||
* hidden input.
|
|
||||||
*/
|
|
||||||
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
----
|
||||||
|
|
||||||
Kotlin::
|
Kotlin::
|
||||||
@@ -846,51 +808,12 @@ class SecurityConfig {
|
|||||||
http {
|
http {
|
||||||
// ...
|
// ...
|
||||||
csrf {
|
csrf {
|
||||||
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() // <1>
|
spa()
|
||||||
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
|
|
||||||
private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
|
|
||||||
private val xor: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
|
|
||||||
|
|
||||||
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
|
|
||||||
/*
|
|
||||||
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
|
|
||||||
* the CsrfToken when it is rendered in the response body.
|
|
||||||
*/
|
|
||||||
xor.handle(request, response, csrfToken)
|
|
||||||
/*
|
|
||||||
* Render the token value to a cookie by causing the deferred token to be loaded.
|
|
||||||
*/
|
|
||||||
csrfToken.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
|
|
||||||
val headerValue = request.getHeader(csrfToken.headerName)
|
|
||||||
/*
|
|
||||||
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
|
|
||||||
* to resolve the CsrfToken. This applies when a single-page application includes
|
|
||||||
* the header value automatically, which was obtained via a cookie containing the
|
|
||||||
* raw CsrfToken.
|
|
||||||
*/
|
|
||||||
return if (StringUtils.hasText(headerValue)) {
|
|
||||||
plain
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
* In all other cases (e.g. if the request contains a request parameter), use
|
|
||||||
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
|
|
||||||
* when a server-side rendered form includes the _csrf request parameter as a
|
|
||||||
* hidden input.
|
|
||||||
*/
|
|
||||||
xor
|
|
||||||
}.resolveCsrfTokenValue(request, csrfToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
----
|
||||||
|
|
||||||
XML::
|
XML::
|
||||||
@@ -899,22 +822,13 @@ XML::
|
|||||||
----
|
----
|
||||||
<http>
|
<http>
|
||||||
<!-- ... -->
|
<!-- ... -->
|
||||||
<csrf
|
<csrf>
|
||||||
token-repository-ref="tokenRepository" <1>
|
<spa />
|
||||||
request-handler-ref="requestHandler"/> <2>
|
</csrf>
|
||||||
</http>
|
</http>
|
||||||
<b:bean id="tokenRepository"
|
|
||||||
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
|
|
||||||
p:cookieHttpOnly="false"/>
|
|
||||||
<b:bean id="requestHandler"
|
|
||||||
class="example.SpaCsrfTokenRequestHandler"/>
|
|
||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
||||||
<1> Configure `CookieCsrfTokenRepository` with `HttpOnly` set to `false` so the cookie can be read by the JavaScript application.
|
|
||||||
<2> Configure a custom `CsrfTokenRequestHandler` that resolves the CSRF token based on whether it is an HTTP request header (`X-XSRF-TOKEN`) or request parameter (`_csrf`).
|
|
||||||
This implementation also causes the deferred `CsrfToken` to be loaded on every request, which will return a new cookie if needed.
|
|
||||||
|
|
||||||
[[csrf-integration-javascript-mpa]]
|
[[csrf-integration-javascript-mpa]]
|
||||||
==== Multi-Page Applications
|
==== Multi-Page Applications
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user