I have a production application that will refresh a csrf token like so
private static final String CSRF_TOKEN_SETTER = "window.import.meta.env.CSRF_TOKEN=\"%s\";";
// sets initial token from request
@GetMapping(value = "/csrf.js", produces = "application/javascript; charset=utf-8")
public String csrf(HttpServletRequest request) {
if (request.getAttribute("_csrf") instanceof CsrfToken token) {
return String.format(CSRF_TOKEN_SETTER, token.getToken());
}
return "ERROR";
}
// used to work, but no longer does after Spring update
@GetMapping(value = "/refresh_csrf_token")
public ResponseEntity<String> refreshCsrfToken(HttpServletRequest request, HttpServletResponse response) {
CsrfToken newToken = csrfTokenRepository.generateToken(request);
csrfTokenRepository.saveToken(newToken, request, response);
return ResponseEntity.ok(newToken.getToken());
}
The FE basically does this:
if (statusCodeIs403) {
refreshCsrfTokenAndAppendToHeadersOfFutureRequests();
}
^ this is because we don't want to surface an error or interrupt the session if the csrf token expires. We just want to refresh it and move on.
This worked on Spring 2. We recently upgraded to Spring 3 and now something seems to be wrong with our implementation. We're able to set the csrf token initially (with a javascript file) but once the csrf token expires and we refresh the token, we still get a 403 returned. Something must have fundamentally changed between Spring 2 and 3 but it's unclear why the refreshed csrf tokens are failing to validate.
Here's our implementation of the CsrfTokenRepository
@Bean
@Lazy
public CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository tokenRepository = new HttpSessionCsrfTokenRepository();
tokenRepository.setHeaderName("X-CSRF-TOKEN");
return tokenRepository;
}
Are we missing a step for saving the token? I also noticed the initial, valid token is a completely different format from the refreshed token:
Valid Token: sBkVwbeM-hgCeILgJFU6zADrNZkMpcnkG3i0vzqSMIR6MWis1Xgk9Y-6mCkvT-DWE3gO9WPeGPs4xvrJKUqCjQ7wCLEZVF6Z
Refreshed Token: 3c78e076-9431-4f70-97a8-0aa8760037a7
Take a look at the diagrams on the updated CSRF chapter of the docs for an overview of the work done by the CsrfFilter
. You are bypassing the filter and therefore don't benefit from the work performed by it.
There are several JavaScript examples but none of them exactly match your customization. My suggestion is to rework your support based on this example or this example.
Specifically, notice that you should be injecting a CsrfToken csrfToken
into your controller method instead of directly interacting with the CsrfTokenRepository
. For example, you can do like this:
@GetMapping(value = "/refresh_csrf_token")
public ResponseEntity<String> refreshCsrfToken(CsrfToken csrfToken) {
return ResponseEntity.ok(csrfToken.getToken());
}