DEV Community

Cover image for LocalStorage vs Cookies: All You Need To Know About Storing JWT Tokens Securely in The Front-End
Michelle Wirantono for Cotter

Posted on • Updated on • Originally published at blog.cotter.app

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 and we 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. This article is mainly based on Please Stop Using Local Storage and the comments to this post.

Local Storage

Pros: It's convenient.

  • It's pure JavaScript and it's convenient. If you don't have a back-end and you're relying on a third-party API, you can't always ask them to set a specific cookie for your site.
  • Works with APIs that require you to put your access token in the header like this: Authorization Bearer ${access_token}.

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 just 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.

  • If you're using httpOnly and secure cookies, that means your cookies cannot be accessed using JavaScript. This means, even if an attacker can run JS on your site, they can't read your access token from the cookie.
  • It's automatically sent in every HTTP request to your server.

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

  • Cookies have a size limit of 4KB. Therefore, if you're using a big JWT Token, storing in the cookie is not an option.
  • There are scenarios where you can't share cookies with your API server or the API requires you to put the access token in the Authorization header. In this case, you won't be able to use cookies to store your tokens.

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 although they rarely have to. It might also be more advantageous for the attacker to attack using 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.1
Host: site.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
Cookie: session=abcdefghijklmnopqrstu

email=myemail.example.com 
Enter fullscreen mode Exit fullscreen mode

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?

  • Both localStorage and cookies are vulnerable to XSS attacks but it's harder for the attacker to do the attack when you're using httpOnly cookies.
  • Cookies are vulnerable to CSRF attacks but it can be mitigated using sameSite flag and anti-CSRF tokens.
  • You can still make it work even if you need to use the Authorization: Bearer header or if your JWT is larger than 4KB. This is also consistent with the recommendation from the OWASP community:

Do not store session identifiers in local storage as the data are always accessible by JavaScript. Cookies can mitigate this risk using the httpOnly flag.

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

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

  • Option 1: Store your access token in localStorage : prone to XSS.
  • Option 2: Store your access token in httpOnly cookie: prone to CSRF but can be mitigated, a bit better in terms of exposure to XSS.
  • Option 3: Store the refresh token in httpOnly cookie: safe from CSRF, a bit better in terms of exposure to XSS. 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, this 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:

  • Use the httpOnly flag to prevent JavaScript from reading it.
  • Use the secure=true flag so it can only be sent over HTTPS.
  • Use the SameSite=strict flag whenever possible to prevent CSRF. This can only be used if the Authorization Server has the same site as your front-end. If this is not the case, your Authorization Server must set CORS headers in the back-end or use other methods to ensure that the refresh token request can only be done by authorized websites.

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 tabs or refresh 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 articles when writing this blog, especially from these articles:


Questions & Feedback

If you need help or have any feedback, feel free to comment here or 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.

Latest comments (46)

Collapse
 
rjbultitude profile image
Rich

Useful article but can you explain why "Option 3...is the best out of the 3 options". Why put the access (bearer) token in web storage and the refresh one in a cookie and not the other way around?

Collapse
 
tweakzyyy profile image
Tweakzy

Hi,
Is it possible to have explaination about that part:

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, this requires the Authorization Server's CORS policy to be set up correctly to prevent requests from unauthorized websites.

I do not understand the following part : "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 me, if the attacker manage to inject Javascript Code through XSS that will send an HTTP to /refreshtoken, then he will be able to read the response, thus retrieve the AccessToken and maybe send it to his malicious external server to us it.

Am I missing something? Thanks a lot !

Collapse
 
iamcrypticcoder profile image
Mahbubur Rahman

This was concise yet effective article to understand the pros and cons of storing JWT in local storage/cookie. But I have a question.

In the firebase auth REST APIs, they are returning both accessToken and refreshToken in the response body. As per Option 3 in your article refresh token shouldn't be inside response. Refresh token should go to Cookie directly so that nobody can access through XSS. Why Google is doing that?

firebase.google.com/docs/reference...

Collapse
 
dorneanu profile image
Victor Dorneanu

Straight to the point! Thanks!

Collapse
 
jaytonic profile image
Jaytonic

Nice article, thank you! One thing I'm not sure I totally understood: About "Store your access token in memory and store your refresh token in the cookie". Doesn't that make us again vulnerable to XSS attacks? Because your in-memory token would be available by some injected javascript, no?

Collapse
 
mjeon profile image
mj • Edited

I only signed up to leave a comment after landing here from a github issue.

I sent a random email to Kevin from the Cotter team a few months back asking for a feedback on something :) - I'd rather not go too specific..
And he was the one of the most friendliest & smartest person I ever met online - even though we had never met before.

Although our current project does not quite fit the bill for cotter usage(because we're a dApp and we have to use MetaMask for authentication), I'd definitely give cotter a shot if we had a chance. I can only imagine the product would be fantastic if smart, nice and hard-working people like Kevin had been working on it. And from my personal experience, I have learnt that the team matters as much as the product itself when choosing a Sass product.

Disclaimer: I have no ties with the cotter team. Just something out of my personal experience with one of the co-founders at Cotter.

Collapse
 
grapes profile image
Grapes

I've a question, if i submit a /refresh_token request in the attack code, can I get the user's access token?

fetch('/refresh_token', {
  credentials: "include"
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rtorcato profile image
Richard Torcato

no, because the refresh token was a httponly, same site cookie unreadable by javascript. If the refresh token cookie is not there /refresh_token should fail.

Collapse
 
zidniryi profile image
zidniryi

Do you think that handling local data on the frontend is better to use GraphQL than Localstorage ?.

Collapse
 
sincness profile image
Maltheboss

Very nice article, it's a good read.

Collapse
 
pankajtanwarbanna profile image
Pankaj Tanwar

I guess if your website is vulnerable to XSS attack, it's game over anyway 😐 JWT token now doesn't matter. What's your thoughts?

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Pankaj, yep I agree with you! It's true that if your site is vulnerable to XSS attack then technically the attacker can do almost whatever they want. However, it is possible to make it harder for the attacker to read/use the access token, which might help in some cases.

Collapse
 
hood profile image
Andrea Cappuccio

We could argue that JWT ain't safe anyway.

Collapse
 
jnareb profile image
Jakub Narębski

Wouldn't using sessionStorage be as secure as using variables to store access token? Both are available from JavaScript for the duration of single session.

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Jakub, yes they are both available from JavaScript for the duration of a single session. However, it might be easier for the attacker to just dump the contents of the session storage compared to trying to find the variable you used for the token.

Collapse
 
ditransler profile image
Dmitriy Beglov

Hi Michelle, thanks for the article.
I'd like to mention one more (possible) con of localStorage. If we create an SSR app then localStorage won't be available for storing tokens.

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt • Edited

I just wonder what is actually accessible by document.cookie?

Secondly would be the implementation. I am interested in all processes from highly-accessible sign-in, to protecting the API endpoint, and the server knows requesters' credentials (for attaching userId in database queries). I currently use Firebase / firebase-admin for these reasons, but I have trouble implementing storing token in cookies. I fear that it might be backend dependent...

I will consider your product.

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Pacharapol!
Cookies that are marked httpOnly are not accessible from document.cookie, otherwise you can access the cookie from document.cookie.
source

With our JS SDK (from yarn add cotter), we actually handle storing the access token in memory and the refresh token in the cookie for you. In short, you can just call:

cotter.tokenHandler.getAccessToken()
Enter fullscreen mode Exit fullscreen mode

and it will:

  • grab the access token from memory if not expired, or
  • automatically refreshes the access token by calling Cotter's refresh token endpoint (where the cookie is included) and return to you a new access token.

If you're interested, shoot me a message on Slack and I can help you with any questions. You can find our documentation here.

Collapse
 
bushblade profile image
Will Adams

Surely the vulnerability here is that you have a site vulnerable to XSS not the choice of where to store the token?

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Will, Putri here – Michelle' cofounder.

Yes, technically if your site is vulnerable to XSS, the attacker can do a lot of damage no matter where you store the token. The options above are intended to help in making it harder for the attacker to obtain the access token itself.