Search code examples
angularspring-bootcsrfx-xsrf-token

CSRF Filter Orders and Random Token Generating


I am experiencing some strange behavior with respect to XSRF token generation, and updating the necesssary cookie values. When I load the landing page of my website--an Angular front-end and a Spring-boot back-end--a XSRF token is generated. Not, ideal but if this is normal and expected then I'm okay with it. The only requests that are generated when going to the landing page are "GET" requests.

After I login to the application, it verifies the first XSRF-TOKEN and validates its validity and proceeds to login. However, a new CSRF token is generated immediately afterward changing the XSRF-TOKEN on the web service. Ergo, the front-end and backend are now out of sync. I'm not sure how to either update the XSRF-TOKEN after a successful login or keep it from being changed since there doesn't appear to be any benefit to that...at least from what I've read.

Web Security

@Override
protected void configure(HttpSecurity http) throws Exception {
    CsrfHeaderFilter csrfHeaderFilter = new CsrfHeaderFilter();

    http.httpBasic()
        .authenticationEntryPoint(new AuthenticationFailureHandler())
        .and()
            .authorizeRequests()
            .antMatchers("List of API URI").permitAll()
            .anyRequest().authenticated();

    // Configurations for CSRF end points
    http.httpBasic()
        .authenticationEntryPoint(new AuthenticationFailureHandler())
        .and()
            .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and()
            .authorizeRequests()
            .antMatchers("Login API URI").permitAll()
        .and()
            .addFilterAfter(csrfHeaderFilter, CsrfFilter.class);

        // Logout configurations
        http.logout()
        .permitAll()
        .logoutSuccessHandler((new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)));
}

The csrf section was separated in the hopes that csrf tokens would only be generated if a post request has sent with that URI. That does not seem to be the case. It seems the CSRF Header filter is called regardless of what URI is sent to the backend.

CSRF Header Filter -- Required because cookie's domain must be updated to allow front and backend to have access to XSRF-TOKEN.

@Override
protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {

    CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
            .getName());

    if (csrf != null) {
      Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
      String token = csrf.getToken();
      if (cookie == null || (token != null && !token.equals(cookie.getValue()))) {
        cookie = new Cookie("XSRF-TOKEN", token);
        cookie.setPath("/");
        cookie.setDomain(<omitted>);
        response.addCookie(cookie);
      }
    }
    filterChain.doFilter(request, response);
  }

I believe there is an issue with how my filters are organized but I've tried everything from combining the different http.httpBasic() sections into a single group to using antIgnores for csrf(), to not processing CSRF tokens with GET requests. Any guidance or suggestions would greatly be appreciated!

If there is anything else that you need let me know and I'll try to provide it.

Thanks!


Solution

  • As I suspected there was an issue with the order of the filtering. There is a filter hierarchy which is established within Spring-Boot Security. The XSRF-TOKEN needed to be generated prior to adding the value to the cookie. If the old value is added to the Cookie before the new value is generated then the front-end and back-end will be out of sync until a browser refresh is performed.

    That's why I added the filter toward the end of the filtering order. If someone has a better entry point, I'd be game to update it accordingly and update this answer to reflect it.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    CsrfHeaderFilter csrfHeaderFilter = new CsrfHeaderFilter();
    
    http.httpBasic()
        .authenticationEntryPoint(new AuthenticationFailureHandler())
        .and()
            .authorizeRequests()
            .antMatchers("List of API URI").permitAll()
            .anyRequest().authenticated();
    
    // Configurations for CSRF end points
    http.httpBasic()
        .authenticationEntryPoint(new AuthenticationFailureHandler())
        .and()
            .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and()
            .authorizeRequests()
            .antMatchers("Login API URI").permitAll()
        .and()
            .addFilterAfter(csrfHeaderFilter, ExceptionTranslationFilter.class);
    
        // Logout configurations
        http.logout()
        .permitAll()
        .logoutSuccessHandler((new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)));
    }