vue.jsspring-securityaxioscsrf

Spring Security 6.2.1 + Vue.js 3 (axios) : Invalid CSRF Token in POST request (code 403)


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.


Solution

  • 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.