From stateful to stateless RESTful security using Spring and JWTs – Part 5 (Stateless CSRF)

During the previous posts in this series, we managed to move from a stateful to a stateless authentication solution using JWTs.

But does our solution hold against cross-site request forgery (CSRF) attacks?
In two words: it depends. In more than two words: it depends on where JWT tokens are stored on the client site. If they are stored as cookies, then some CSRF defense is worth the extra effort.

So pack your bag and fire up your IDE! (or let Git do the work by pointing it to this GitHub tag. The choice is yours!).

Related posts

Know thy enemy

Imagine you are currently logged to mysupadupasite.com. You know the site is pretty much secure, so no worries there.
But then open a new tab and browse to mydevilevilsite.com. You have not actually logged out of the supadupa site, so the cookie where the session ID or JWT is stored is still there.

Given that mydevilevilsite.com is… evil indeed, it will leverage that lingering cookie and send to mysupadupasite.com a request such a:

POST /mysavings/beneficiary/?action=set&to=Dr%20Evil%201600%20Pennsylvania%20Avenue

How’s that possible? Well, mydevilevilsite cannot see what is written in the mysupadupasite cookie, because it’s not set for the mydevilevilsite domain. But any request sent to mysupadupasite will automatically include any cookie set for it! So when the evil site does the request, the login cookie will be sent along with it. In practice that means that, with some knowledge of the API being targeted, mydevilevilsite can perform actions on your behalf as long as the login cookie on your browser is valid and present.

Which brings us to the important question: as an adopter of stateless, JWT-based authentication, does this concern me?

Yes, but only if you store your JWT in a cookie.

In a forthcoming post we will see that storing JWTs in a cookie, even an http-only cookie, may not the best option. Then again, maybe you are in a situation where you absolutely need to store your tokens in cookies? If that is indeed the case, then keep on reading.

CSRF protection: the stateless way

In a stateful situation, the back end would generate and send the browser a token which has to be copied to an HTTP header before a request to the API endpoint is made. This way the server gets the token from the header, compares it with the one he sent to that browser for that session, and if both tokens match then it means the API request can be green-lighted.
This works because only the original website is able to read the cookie with that anti-CSRF token and copy it to the header. An evil website would not be able to read the cookie’s content, so it would not be able to copy it to the header. At best, it would be able to send that token cookie back to the server. That’s why it is important to have the cookie’s content (the token) copied to the header.

In a stateless situation the server will not remember which user got which CSRF token in order to verify it it was rightfully sent back at the next API request. That would mean that the server would have to maintain a user’s state, a session, which is exactly what we want to avoid.
However the back end still can check whether he got the same token as a cookie and as a HTTP header. That allows the server to make sure the API request is not a cross-site request forgery, since only the original website is able to write that token in a cookie with the right domain attribute. This is called the double submit cookie.

Implementing stateless CSRF

The client (the browser) will have to generate a decently-random (cryptographically strong) value that it will send with its request to the back end API. Thanks to the WebCrypto API, the generation of these values is quite easy to accomplish.
In the context of our JUnit test, we will simply use random UUIDs.

The improved version of the TestRestClient class we use testing our API endpoints now adds the generation of these tokens when logging in or when POSTing a request to the API:

...
public <T> ResponseEntity<T> post(String restPath, Credentials credentials, Object body, Class<T> responseType, String csrfToken) {
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.AUTHORIZATION, credentials.token);
    if (csrfToken != null) {
        headers.set(HttpHeaders.COOKIE, SecurityConfiguration.CSRF_COOKIE + "=" + csrfToken);
        headers.set(SecurityConfiguration.CSRF_HEADER, csrfToken);
    }
    return rest.exchange(restPath, POST, new HttpEntity<>(body, headers), responseType);
}

public Credentials login(String username, String password) {
    return rest.execute(
            "/login",
            POST,
            request -> {
                // Body
                OutputStream body = request.getBody();
                body.write(("username=" + username + "&password=" + password).getBytes());
                body.flush();
                body.close();

                // Headers
                HttpHeaders headers = request.getHeaders();
                String csrfToken = UUID.randomUUID().toString();
                headers.set(HttpHeaders.COOKIE, SecurityConfiguration.CSRF_COOKIE + "=" + csrfToken);
                headers.set(SecurityConfiguration.CSRF_HEADER, csrfToken);

            }, response -> {
                Credentials credentials = null;
                List<String> authorizationTokens = response.getHeaders().get(HttpHeaders.AUTHORIZATION);
                if (authorizationTokens != null && authorizationTokens.size() > 0) {
                    credentials = new Credentials(authorizationTokens.get(0));
                }
                return credentials;
            }
    );
}

public static class Credentials {
    public String token;
    public Credentials(String token) {
        this.token = token;
    }
}
...

As a reminder: this TestRestClient class’ scope is to simulate what a web-based application would do to access the RESTful endpoints.

The back end simply has to verify that the token values from the received HTTP header and cookie are equal. A filter will be used for that purpose:

package be.codesandnotes.security;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import static be.codesandnotes.security.SecurityConfiguration.*;

public class StatelessCsrfFilter extends OncePerRequestFilter {

    private static final Set<String> SAFE_METHODS = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

    private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

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

        if (csrfTokenIsRequired(request)) {
            String csrfHeaderToken = request.getHeader(CSRF_HEADER);

            String csrfCookieToken = null;
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                Optional<Cookie> csrfCookie = Arrays.stream(cookies)
                        .filter(cookie -> cookie.getName().equals(CSRF_COOKIE))
                        .findFirst();
                if (csrfCookie.isPresent()) {
                    csrfCookieToken = csrfCookie.get().getValue();
                }
            }

            if (csrfHeaderToken == null || csrfCookieToken == null || !csrfCookieToken.equals(csrfHeaderToken)) {
                accessDeniedHandler.handle(request, response, new AccessDeniedException("CSRF tokens missing or not matching"));
                return;
            }
        }

        filterChain.doFilter(request, response);
    }

    private boolean csrfTokenIsRequired(HttpServletRequest request) {
        return !SAFE_METHODS.contains(request.getMethod());
    }
}

Connecting this filter to the security chain is as simple as modifying one line of configuration in the SecurityConfiguration class:

...
protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .addFilterBefore(statelessCsrfFilter, CsrfFilter.class);
        ...
}
...

And that’s it!

Feel free to run the tests in SecurityConfigurationTest to make sure that our filter is properly blocking any request that’s missing its CSRF tokens.

So, we’re done now?

My objective with this tutorial series was to provide a comprehensive overview on how to transition from stateful to stateless security: to leverage JWTs in conjunction with Spring Boot and Spring Security to obtain a basic, but functional stateless security solution. Strictly speaking, you can start implementing this in your project right now!

But should you?

No security solution is 100% perfect, and stateless security has its shortcomings too. Maybe the right choice for your project still is session-based authentication?
That’s why my next post will attempt discussing the pros and cons of stateless and stateful security solutions.

Until then,

Cheers!

2 comments

  1. Lluis Reply
    03/12/2018 at 13:54

    Hi!
    I’m developing a website which has SpringBoot in backend and NuxtJS in frontend. With nuxt, you can access server-side properties which include the request send back and froth by the browser, and that implies having access to the headers and cookies.

    So the question is, if the backend API sends the JWT in a hardened cookie (httpOnly, secure and same-site) back to the client, BUT EXPECTS the token in the Authorization header (so in my nuxt app, once the login is done, I can access the cookie stored by the backend and put its value to an authorization header in every ajax request I do), do I need to implement XSS and CSRF protection? (I already have CSRF protection BTW)

    • Diego Reply
      03/12/2018 at 19:42

      Hello!

      In my opinion I would keep the CSRF and Auth tokens separate. The thing with the secure Auth cookie is, should an attacker manage to inject some malevolent script in your front-end, he still cannot access the actual content of that cookie, which is the token. So he cannot forge it. But if you use that JWT as the CSRF token then it becomes easier to read and steal your Auth token. So now the attacker can impersonate you by going to the website, create his own account but manually replace his cookie with yours. Oops!

      But if you handle CSRF with a different token and keep the cookie unexposed, it becomes harder for an attacker to steal your account. Now he cannot steal the actual JWT from your cookie.

      Of course, he could always inject a more complex script that performs requests within the web application itself: as soon as the script is executed from inside your app, it can generate and write a CSRF token into header and cookie (same domain cookie), and it doesn’t need to know about your Auth token because it will be sent with the request. So yeah, it’s not 100% secure and the trick there would be to ensure you’re protected against XSS. OWASP has a full cheat sheet on that https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) but frameworks like Angular are helping a lot on that level.

      In conclusion: I would separate CSRF and Auth tokens and work on that XSS prevention as much as possible. But if you think that XSS protection is out of reach (not the right front end framework, not enough time to go through that OWASP sheet…) then I would seriously consider the “session” option instead.

      Hope this helps!

Leave A Comment

Please be polite. We appreciate that. Your email address will not be published and required fields are marked

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: