I'm creating a Single Page application with Spring and Vue, but I cannot get the anti-csrf protection to work. I followed what this topic suggests but the CSRF token isn't found with any name (_csrf, X-XSRF-TOKEN, X-XSRF, X-CSRF-TOKEN) in my POST request although the header SET-COOKIES with the token is in the response.
Here is my security configuration :
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
return http.build();
}
}
final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = 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.delegate.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
/*
* 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.
*/
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
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
}
}
final class CsrfCookieFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(CsrfCookieFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("X-CSRF-TOKEN");
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}
And here is the request I’m making to the server.
export async function validateObservationsId(producer_id: string, dataset_id: string, document_ids: string[]): Boolean[] {
const url = `${myBaseUrl}/download/observation/${producer_id}/${dataset_id}/exists`;
// Récupérer le token CSRF depuis le cookie
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
console.log("CSRF Token: ", csrfToken);
try {
let response = await axios.post(
url,
document_ids,
{
headers: { 'X-CSRF-TOKEN': csrfToken }
}
);
return response.data;
} catch (error) {
console.error('Error validating observation IDs', error);
return [];
}
}
According to what I know, Axios should add the token to the request by itself, but I’ve tried with and without it, and neither works.
After doing some more research, I found a solution that worked for me:
My client runs on localhost:8080
, while the server is on localhost:8084
. This setup makes my POST request a "cross-site" request. To allow Axios to handle this properly, I needed to enable credentials and support for the CSRF token. Here’s how I configured it:
export let axiosInstance = Axios.create({
baseURL: apiURL,
});
// New lines to allow credentials and CSRF token handling
axiosInstance.defaults.withCredentials = true;
axiosInstance.defaults.withXSRFToken = true;
The line axiosInstance.defaults.withCredentials = true;
instructs Axios to include cookies and credentials in each request. Additionally, the line axiosInstance.defaults.withXSRFToken = true;
enables Axios to automatically add the X-XSRF-TOKEN
header in requests.
On the server side, I also needed to allow credentials by adding
@CrossOrigin(origins = "http://localhost:8080", allowCredentials = "true")
above my controller. This configuration ensures that the browser can send cookies with requests, but only if they originate from http://localhost:8080
, thereby enhancing security by restricting requests to that specific origin.