What’s the Secure Way to Store JWT?

Web app anti-CSRF & anti-XSS playbook

Yang Liu
The Startup
4 min readJul 23, 2020

--

TL; DR

There’re 2 major ways to store the JWT in the frontend.

  • A: In the local storage and send it via a custom header.
  • B: In a secure httpOnly cookie.

For method A, it’s CSRF-safe but is vulnerable to XSS. For method B, it’s XSS-safe but is vulnerable to CSRF.

Luc Engelen’s opinion (also mine): CSRF is easier to deal with but the amount of work to fight XSS is proportional to the size of the frontend. Hence the method B is preferable.

Prelude: what’s a JSON Web Token?

Here’s an introduction.

JWT, from a cryptography perspective, it only ensures integrity. So the token itself standalone is not a good approach to implement an authentication flow — anyone who got the token can impersonate you!

But since it’s stateless, which means the app owners can cut the budget of the backend servers, it’s still very popular in this era.

So the baseline of using JWT is you must ensure the whole internet traffic is encrypted, typically protected by HTTPS.

Prelude: CSRF & XSS

After that, the 2 major enemies are:

  1. Cross-site Request Forgery (CSRF) attacks. If your JWT is sent along with every HTTP request (when you use method B), the hacker is able to send requests on behalf of you.
  2. Cross-site scripting (XSS) attacks. If your JWT is accessible to JavaScript (when you use method A) then any attacker who can inject the script to the site can do anything and tear down the CSRF countermeasures easily.

The countermeasures

In the following sections, I will go to the details of implementing security methods to protect JWT from CSRF & XSS when we save the token in the cookie.

Secure the cookie

  1. Ensure cookies are secure : it can only be seen in HTTPS.
  2. Ensure cookies are httpOnly : the JavaScript code can’t read it.
  3. Set SameSite attribute.
  4. Use __Host- cookie prefix to prevent from insecure sub-domain changing the cookie in the top-level domain.
  5. Encrypt and sign the cookie value with the server secret.

You can verify your website’s cookie configuration via a Chrome extension. Take “EditThisCookie” extension as the inspector, the ideal configuration would look like this:

ideal cookie configuration

With this setting, a Cross-site scripting attack can never steal your JWT.

Use a custom header to prevent CSRF

Because a cross-site request is not allowed to send with custom headers, the request will never succeed if the backend rejects all requests failed the header test.

A typical header name would be X-CSRF-Token . All the responses from the backend will carry this header, and whenever the frontend is going to make a POST/PUT/PATCH/DELETE call, it should send with the same header with the same value. The token could be valid only for one request, so it’s the frontend’s responsibility to keep the CSRF token up-to-date.

It seems that the server goes back to stateful. Because it needs to keep tracking the state of tokens. But we can use a so-called double-submit-cookie technique to solve the problem. To put it short, we send the same token both into the cookie and into the header. The server checks the token by matching the cookie and the header.

Login CSRF

I read many of the StackOverflow discussions. And in many places, people are saying the CSRF token for login form is bad. This is wrong! Maybe this misconception is a major reason why there’re so many vulnerabilities found in the OAuth2 flow. The following quote comes from the CSRF Prevention Cheatsheet from OWASP.

CSRF vulnerabilities can still occur on login forms where the user is not authenticated, but the impact and risk is different.

For example, if an attacker uses CSRF to authenticate a victim on a shopping website using the attacker’s account, and the victim then enters their credit card information, an attacker may be able to purchase items using the victim’s stored card details.

To make this right, we should

  1. Create sessions for visitors, or at least for ones who are about to log in.
  2. Create a new session after the user is logged in. This is to prevent Session-fixation attacks.

A sample OAuth flow

Here’s a sample OAuth flow with login CSRF protection. Basically the endpoint/auth/handshake could be anything as long as the frontend can get the very first CSRF token before login. It’s also good to use a POST method because by the definition of REST API (and in other conventions), results from GET is cacheable.

Conclusion

As you can see, for a web app, the cookie-based JWT needs a relatively simple way to counter CSRF and XSS. Especially when you compare this to the endless fear of “the new intern introduced an XSS vulnerability in our new page” when you use a header-based JWT.

References

[1]: https://blog.eq8.eu/article/rails-api-authentication-with-spa-csrf-tokens.html

[2]: https://dev.to/gkoniaris/how-to-securely-store-jwt-tokens-51cf

[3]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

[4]: https://www.sjoerdlangkemper.nl/2017/02/09/cookie-prefixes/

[5]: https://www.theguild.nl/where-to-put-json-web-tokens-in-2019/

[6]: http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/

--

--