LocalStorage vs. Cookies: All You Need to Know About Storing JWT Tokens Securely in the Front-End

JWT Tokens are awesome but how do you store them securely in your front end? We'll go over the pros and cons of localStorage and Cookies.

We went over how OAuth 2.0 works in the last post which covered how to generate access tokens and refresh tokens. The next question is: how do you store them securely in your front-end?

A Recap about Access Token & Refresh Token

Access tokens are usually short-lived JWT Tokens, signed by your server, and are included in every HTTP request to your server to authorize the request.

Refresh tokens are usually long-lived opaque strings stored in your database and are used to get a new access token when it expires.

Where should I store my tokens in the front end?

There are 2 common ways to store your tokens: in localStorage or cookies. There are a lot of debate on which one is better and most people lean toward cookies for being more secure.

Let’s go over the comparison between localStorage and cookies. This article is mainly based on Please Stop Using Local Storage and the comments to this post.

Local Storage

Pros: It's convenient.

Cons: It's vulnerable to XSS attacks.

An XSS attack happens when an attacker can run JavaScript on your website. This means that the attacker can take the access token that you stored in your localStorage.

An XSS attack can happen from a third-party JavaScript code included in your website like React, Vue, jQuery, Google Analytics, etc. It's almost impossible not to include any third-party libraries in your site.

Cookies

Pros: The cookie is not accessible via JavaScript; hence, it is not as vulnerable to XSS attacks as localStorage.

Cons: Depending on the use case, you might not be able to store your tokens in the cookies.

About XSS Attack

Local storage is vulnerable because it's easily accessible using JavaScript and an attacker can retrieve your access token and use it later. However, while httpOnly cookies are not accessible using JavaScript, this doesn't mean that by using cookies, you are safe from XSS attacks involving your access token.

If an attacker can run JavaScript in your application, then they can just send an HTTP request to your server and that will automatically include your cookies. It's just less convenient for the attacker because they can't read the content of the token but they rarely have to. It might also be more advantageous for the attacker to attack on the victim's browser (by just sending that HTTP Request) rather than using the attacker's machine.

Cookies and CSRF Attack

CSRF Attack is an attack that forces a user to do an unintended request. For example, if a website is accepting an email change request via:

POST /email/change HTTP/1.1Host: site.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
Cookie: session=abcdefghijklmnopqrstu

email=myemail.example.com 

Then an attacker can easily make a form in a malicious website that sends a POST request to https://site.com/email/change with a hidden email field, and the session cookie will automatically be included.

However, this can be mitigated easily using sameSite flag in your cookie and by including an anti-CSRF token.

Conclusion

Although cookies still have some vulnerabilities, it's preferable compared to localStorage whenever possible. Why?

This is also consistent with the recommendation from the OWASP community:

Do not store session identifiers in local storage as the data is always accessible by JavaScript. Cookies can mitigate this risk using the httpOnly flag. - OWASP: HTML5 Security Cheat Sheet

So, how do I use cookies to persist my OAuth 2.0 tokens?

As a recap, here are the different ways you can store your tokens:

We’ll go over how Option 3 works as it is the best out of the 3 options.

Store your access token in memory and store your refresh token in the cookie

Why is this safe from CSRF?

Although a form submit to /refresh_token will work and a new access token will be returned, the attacker can't read the response if they're using an HTML form. To prevent the attacker from successfully making a fetch or AJAX request and read the response, it requires the Authorization Server's CORS policy to be set up correctly to prevent requests from unauthorized websites.

So how does this set up work?

Step 1: Return Access Token and Refresh Token when the user is Authenticated.

After the user is authenticated, the Authorization Server will return an access_token and a refresh_token. The access_token will be included in the Response body and the refresh_token will be included in the cookie.

Refresh Token cookie setup:

Step 2: Store the access token in memory

Storing the token in-memory means that you put this access token in a variable in your front-end site. Yes, this means that the access token will be gone if the user switches to another tab or refreshes the site. That's why we have the refresh token.

Step 3: Renew access token using the refresh token

When the access token is gone or has expired, hit the /refresh_token endpoint and the refresh token that was stored in the cookie in step 1 will be included in the request. You'll get a new access token and can then use that for your API Requests.

This means your JWT Token can be larger than 4KB and you can also put it in the Authorization header.

That's It!

This should cover the basics and help you secure your site. This post is written by the team at Cotter – we are building lightweight, fast, and passwordless login solution for websites and mobile apps.

If you're building a login flow for your website or mobile app, these articles might help:


References

We referred to several of articles when writing this blog, especially from these articles:


Questions & Feedback

If you need help or have any feedback, ping us on Cotter's Slack Channel! We're here to help.

Ready to use Cotter?

If you enjoyed this post and want to integrate Cotter into your website or app, you can create a free account and check out our documentation.

Made in Typedream