Describe the bug
In Spring Security 5.x and 6.x before RC1 includes the new XSRF token in the login response so it is immediately available in the browser. The code in CsrfAuthenticationStrategy.onAuthentication is
this.csrfTokenRepository.saveToken(null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
In 6 RC1 the code only deletes the old token and does not generate nor save a new one
this.tokenRepository.saveToken(null, request, response);
If the login request is a standard browser POST, then it is typically followed by a page reload which loads the new token. However when the login request is an XHR (fetch), the cookie should be included in the login response as it will not be followed by a new request. This does not happen with RC1.
To Reproduce / Sample
@Bean(name = "MySecurityFilterChainBean")
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers(new AntPathRequestMatcher("**")).authenticated();
http.formLogin();
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
Login and check the POST response in the browser. There is only
Set-Cookie: XSRF-TOKEN=; Max-Age=0; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/
Expected behavior There should also be the new token included so the following fetch/xhr works correctly.
Comment From: Artur-
I managed to work around this by passing the CsrfTokenRepository to our AuthenticationSuccessHandler and adding the code which was removed from Spring Security:
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
...
csrfTokenRepository.saveToken(csrfTokenRepository.generateToken(request), request, response);
Comment From: sjohnr
@Artur-, thanks for reaching out! I saw your gitter question as well but I'll post an answer here for posterity.
One of the themes of Spring Security 6 is smarter session management, which seeks to defer or avoid session access until it is absolutely necessary. Along with that theme, we've addressed gh-4001 which is a long-standing issue that required changes to how CSRF tokens are rendered in html pages for defense in-depth.
The culmination of these two efforts resulted in adding a new default method to CsrfTokenRepository with signature:
* DeferredCsrfToken loadDeferredToken(HttpServletRequest request, HttpServletResponse response);
The CsrfAuthenticationStrategy now utilizes this method to create a new deferred token and store it as a request attribute. See Configure CsrfTokenRequestHandler in the 5.8 docs for details.
How you access the CsrfToken has not changed, but the fact that it is not automatically sent to the response (when using CookieCsrfTokenRepository with CsrfAuthenticationStrategy) has changed. See the very next section, which details how to access the CsrfToken from request attributes. Unfortunately, the documentation does not have an example specific to XHR clients that never utilize html pages from the server, so additional work to include the CSRF token in some response is required.
Note: This would have always been required, except as you point out, the token was previously eagerly refreshed by the CsrfAuthenticationStrategy and rendered to the response, so servlet applications would have never needed to provide a mechanism for providing the CSRF token to the client before.
I will take this issue as feedback that we need to provide some guidance for migrating applications to 6.0 and how to automatically include the CSRF token in the response if needed. How you would do so depends a bit on how the application is set up. But because you're looking for the existing behavior to be preserved, you could do so in a couple of ways:
- Using an
AuthenticationSuccessHandler - Using a custom
Filter(that runs after authentication success) - Using a custom
@ControllerAdvicesimilar to WebFlux - In other situations (not this one), you can also use a custom controller endpoint
In all cases, you have simply to access the CsrfToken from request attributes, like so:
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
csrfToken.getToken(); // causes the deferred token to be rendered to the response when using CookieCsrfTokenRepository
This will (as noted above) add it as a cookie to the response. The deferred nature of the CsrfToken means any access to it causes this to happen. To go a bit farther (and make the code less awkward), you can use the response to also render it as a header if desired:
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
Given that this is the desired behavior, I'm going to close this issue as answered for now. Though I will address this via our upcoming migration guide which will document it better. If I have misunderstood anything, or you still believe this to be a bug, feel free to add additional comments and we can re-open if needed.
Comment From: sjohnr
@Artur- just a heads up that we have begun working on a Migration Guide which includes information about the Deferred CsrfToken. The guide documents how you can revert to the old (if needed) behavior by locking in the 5.8 default instead of accepting the 6.0 default.
Comment From: Artur-
Thanks, we fixed it in https://github.com/vaadin/flow/pull/14932/files by passing around the CsrfTokenRepository and using it in the success handler.
In all cases, you have simply to access the CsrfToken from request attributes, like so: CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
If I remember correctly this is what the code should have fallen back to but that seemed to refer to the old csrf token and not generate a new one after login.
Comment From: sjohnr
If I remember correctly this is what the code should have fallen back to but that seemed to refer to the old csrf token and not generate a new one after login.
I've just double-checked this and don't see that to be the case. The new token is immediately reflected in an AuthenticationSuccessHandler using the above snippet of code. If you do have an example where this is the case, it could be due to mis-configuration but we can certainly take a look.
Comment From: sjohnr
Actually, what I said above is true for HttpSessionCsrfTokenRepository. I'm still looking into this for the CookieCsrfTokenRepository.
Comment From: Artur-
This is CookieCsrfTokenRepository. I just re-checked that I have a xsrf like d5e5... before logging in and after the xsrf has been cleared by Spring Security and my success handler is reached then
((CsrfToken)request.getAttribute(CsrfToken.class.getName())).getToken()
returns the same d5e5... value
Running csrfTokenRepository.saveToken does not change this either
Comment From: sjohnr
Thanks @Artur-!
Apologies, I had not tested all the way through pre-5.8 through 6.0 with CookieCsrfTokenRepository only, and in fact there does appear to be a bug in how it works in both 5.8. and 6.0 with CsrfAuthenticationStrategy. Thanks for mentioning that the attribute didn't update even when accessing the attribute manually as I suggested.
The intended behavior in 6.0 is to defer loading the token, so the original issue you reported here is still the intended behavior and not a bug. The bug shows up when you attempt to access the attribute in the same request where it was nulled out (which the login response will no longer do by default).
I'm going to open a new issue to address the bug in CookieCsrfTokenRepository, which is actually an issue in 5.8.