0

I have a "customer detail" form that submits a POST request. This is in a Spring Boot application and is implemented as a Thymeleaf template. I have just added a little AJAX function to the form -- when the user clicks a certain button, it uses jQuery.ajax() to look up a geo-code based on the customer's address. The AJAX call is a GET request.

If I enter the customer form and click "Submit" without touching the Ajax button, it submits normally. But if I use the Ajax button first, and then try to submit the form, it gets stopped by the CsrfFilter with a message "Invalid CSRF token found for http://...".

I assume that the Ajax call is somehow making the CSRF token expire, so that a new one is needed, but how can I prevent that?

There are some similar-sounding questions like this one but in that case they're looking for a way to add a CSRF token to an Ajax request. Also, I'm using Thymeleaf views so I get the CSRF token automatically inserted into my form -- I don't have to add it manually.

workerjoe
  • 2,421
  • 1
  • 26
  • 49

1 Answers1

0

The problem was that I had implemented a custom filter to delete the security context on every request. This was my way of making the application stateless, as I documented in this software engineering SE answer. That deletes (or resets?) the server-side CSRF token so it won't match the one in the form, however, the custom filter fires after the CsrfFilter so I never had a problem with form submission before.

In this case though I have Ajax requests that are handled after the form is built for the client but before it is submitted, so my custom filter's unfortunate side effect causes form submission to fail.

I corrected it by modifying the custom filter to skip deleting the context if the URL is under a particular directory path:

public class SecurityContextDeletingFilter extends GenericFilterBean {

    RequestMatcher exceptedPaths = new AntPathRequestMatcher("/form-ajax/**");

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpSession session = request.getSession();

        if( exceptedPaths.matches(request)) {
            // Deleting the security context deletes the CSRF token in the session. We want to make sure this
            // does NOT occur if the user is in a form and is using Ajax features like the geocode lookup,
            // because they'll need the server to recognize the token on eventual form submission.
            logger.trace("Skipping this filter for a /form-ajax/ endpoint.");
        } else {
            if( session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY) != null ) {
                session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
            }
        }

        filterChain.doFilter(servletRequest,servletResponse);
    }
}
workerjoe
  • 2,421
  • 1
  • 26
  • 49
  • What do you mean in stateless? No point in having `session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);` as SessionManagementFilter later in filter chain will check if SecurityContext contains authenticated user and if yes it will store authentication in session under `SPRING_SECURITY_CONTEXT_KEY` key. So basically you remove it in your custom filter but later SessionManagementFilter will put it back on the same request. Read 15.1 https://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/session-mgmt.html – S.Step Apr 06 '20 at 19:17
  • If you read the linked post, I authenticate the user on every request based on a custom JWT cookie (not the JSESSIONID cookie). When the user clicks "sign out", I delete the JWT cookie, but previously, Spring was keeping the user authenticated via the JSESSIONID cookie, so effectively they couldn't log out. The SecurityContextDeletingFilter solves that problem. – workerjoe Apr 06 '20 at 20:06