Improve CSRF example for single-page apps

Closes gh-15105
This commit is contained in:
Steve Riesenberg 2024-05-29 11:52:09 -05:00
parent 17064fc7fb
commit ee9f5a2d5e
No known key found for this signature in database
GPG Key ID: 3D0169B18AB8F0A9

View File

@ -788,14 +788,14 @@ public class SecurityConfig {
.csrf((csrf) -> csrf .csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1> .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <1>
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2> .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // <2>
) );
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); // <3>
return http.build(); return http.build();
} }
} }
final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler { final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler(); private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
@Override @Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) { public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
@ -803,40 +803,28 @@ final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body. * the CsrfToken when it is rendered in the response body.
*/ */
this.delegate.handle(request, response, csrfToken); this.xor.handle(request, response, csrfToken);
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get();
} }
@Override @Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/* /*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler * If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes * to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the * the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken. * raw CsrfToken.
*/ *
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
/*
* In all other cases (e.g. if the request contains a request parameter), use * In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a * when a server-side rendered form includes the _csrf request parameter as a
* hidden input. * hidden input.
*/ */
return this.delegate.resolveCsrfTokenValue(request, csrfToken); return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}
final class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.getToken();
filterChain.doFilter(request, response);
} }
} }
---- ----
@ -860,31 +848,36 @@ class SecurityConfig {
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2> csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() // <2>
} }
} }
http.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) // <3>
return http.build() return http.build()
} }
} }
class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() { class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler() private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
private val xor: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) { override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
/* /*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body. * the CsrfToken when it is rendered in the response body.
*/ */
delegate.handle(request, response, csrfToken) 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? { override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
val headerValue = request.getHeader(csrfToken.headerName)
/* /*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler * If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes * to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the * the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken. * raw CsrfToken.
*/ */
return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) { return if (StringUtils.hasText(headerValue)) {
super.resolveCsrfTokenValue(request, csrfToken) plain
} else { } else {
/* /*
* In all other cases (e.g. if the request contains a request parameter), use * In all other cases (e.g. if the request contains a request parameter), use
@ -892,19 +885,8 @@ class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
* when a server-side rendered form includes the _csrf request parameter as a * when a server-side rendered form includes the _csrf request parameter as a
* hidden input. * hidden input.
*/ */
delegate.resolveCsrfTokenValue(request, csrfToken) xor
} }.resolveCsrfTokenValue(request, csrfToken)
}
}
class CsrfCookieFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val csrfToken = request.getAttribute("_csrf") as CsrfToken
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.token
filterChain.doFilter(request, response)
} }
} }
---- ----
@ -918,21 +900,18 @@ XML::
<csrf <csrf
token-repository-ref="tokenRepository" <1> token-repository-ref="tokenRepository" <1>
request-handler-ref="requestHandler"/> <2> request-handler-ref="requestHandler"/> <2>
<custom-filter ref="csrfCookieFilter" after="BASIC_AUTH_FILTER"/> <3>
</http> </http>
<b:bean id="tokenRepository" <b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository" class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/> p:cookieHttpOnly="false"/>
<b:bean id="requestHandler" <b:bean id="requestHandler"
class="example.SpaCsrfTokenRequestHandler"/> class="example.SpaCsrfTokenRequestHandler"/>
<b:bean id="csrfCookieFilter"
class="example.CsrfCookieFilter"/>
---- ----
====== ======
<1> Configure `CookieCsrfTokenRepository` with `HttpOnly` set to `false` so the cookie can be read by the JavaScript application. <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`). <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`).
<3> Configure a custom `Filter` to load the `CsrfToken` on every request, which will return a new cookie if needed. 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