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.

Top comments (46)

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

The part of this discussion I always stumble over is when it is recommended to "just" use anti-CSRF tokens. This is a non-trivial requirement. It is easy for one server -- most of them have built-in libs just like with JWT authentication. However, unlike JWT authentication it is a stateful process. So once you go beyond a single API server (including a fail-over scenario) you have to externalize the issued CSRF tokens into something like Redis (or a DB if you don't mind even more added latency). So all servers can be aware of the issued tokens. This adds another infrastructure piece that needs to be maintained and scaled for load. Edit: I guess people already using session servers are thinking "So what, we already have Redis to track user sessions." But with JWT, user sessions are stateless (just the token they provide and you validate) so this extra infrastructure isn't needed. That's a maintenance cost eliminated.

As far as local storage being vulnerable to XSS attacks, OWASP also puts out an XSS Prevention Cheat Sheet. The main attack vector for XSS is when you allow users to directly input HTML/JS and then execute it. Most major frameworks already santize user inputs to prevent this.

Modern JavaScript frameworks have pretty good XSS protection built in.

  • OWASP XSS Prevention Cheat Sheet

The less common threat that you mentioned was NPM libraries becoming subverted to include XSS attacks. NPM has added auditing tools to report this and warn users. (Edit: Fair point is that people sometimes still use JS libs from CDNs, which may have less scrutiny.) And also Content Security Policy is supported in all major browsers and can prevent attacks and the exfil of token/data even if a script on your site gets compromised. It does not necessarily prevent the compromised script from making calls to your own API. But they would have to be targeting your API specifically to accomplish much.

I completely understand the recommendation to use cookies + Secure + HttpOnly + anti-forgery tokens from a security perspective. And as far as I am aware it is superior security to JWT in local storage. But it also has pretty significant constraints. And local storage is not bad, security-wise. It is isolated by domain. XSS attacks are already heavily mitigated by just using a modern JS framework and paying attention to NPM audit warnings. Throw in CSP for good measure. And of course not going out of your way to evaluate user-entered data as HTML/JS/CSS. (If your site functionality requires this, then you probably should use cookie auth and CSP.)

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Kasey, thanks for your comment! I do agree that localStorage is not bad at all, and considering how XSS attacks are already heavily mitigated as you mentioned, it's a valid option.

Collapse
 
kspeakman profile image
Kasey Speakman

Hey thanks for the response! Best wishes.

Collapse
 
toddmath profile image
Todd Matheson

Great article. Thanks for the in depth research and clear tutorial. Logic was very concise. 😃

Collapse
 
michelle profile image
Michelle Wirantono

Happy to help! Feel free to ping me if you have any questions/concerns :)

Collapse
 
toddmath profile image
Todd Matheson

Thanks

Collapse
 
anshulnegitc profile image
Anshul Negi

Was in a long search for this clarification.
Thanks

Collapse
 
michelle profile image
Michelle Wirantono • Edited

Thanks Anshul! Let me know if you want me to discuss any other topics related to Authentication :)

Collapse
 
anshulnegitc profile image
Anshul Negi

For sure
As for now, this article clears most of the doubts maybe in future if I lost around something related to authentication, will let you know.

Collapse
 
lucienglue profile image
Lucien glue

Thanks for this article, it helped me a lot!

Collapse
 
michelle profile image
Michelle Wirantono • Edited

Thanks Lucien! Let me know if you have any questions :)

Collapse
 
octaneinteractive profile image
Wayne Smallman • Edited

If you use Express, then it could be worth looking at Express Session and the option to save the data to Redis:

app.use(
  session({
    name: 'sessionForApplication',
    secret: process.env.SESSION_SECRET,
    saveUninitialized: true,
    resave: true,
    cookie: {
      expires: expiryDate,
      domain: process.env.APP_DOMAIN
    },
    store: new RedisStore(optionsForRedis)
  })
)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
hemant profile image
Hemant Joshi

Yes, redis is the best one🙂, also cookies would be my second option for JWT based storage

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Wayne, Putri here – Michelle's cofounder.

This is very helpful, Express Session with Redis is definitely a great option. Thanks for the comment!

Collapse
 
octaneinteractive profile image
Wayne Smallman

A pleasure, and glad to help.

Collapse
 
m4r4v profile image
m4r4v

Very descriptive and helpful article.
Thanks!!!

Collapse
 
michelle profile image
Michelle Wirantono • Edited

Thanks Jorge!

Collapse
 
krukru profile image
Marko Kruljac • Edited

Hi Michelle, really great article!
What always confused me about httpOnly cookies and JWT is that the frontend app is missing a big benefit of JWT, which is the payload containing claims and possibly other custom data from the backend.
This is most often the user's role, which then the app uses to render privileged parts of the UI and so on, or the token expiry information. With httpOnly, this benefit is not utilised - but the cost in increased packet size is still being paid!
There are strategies which take option 3 to the extreme, and people have already written great articles about this in details, that the JWT token itself should be split into 2 parts, it's signature in httpOnly, and the rest in a normal JS-accessible cookie. This ofcourse increases the complexity of the backend as well, which now needs to piece together the final JWT from two different incoming sources. I guess this could be option 4.

It seems to me, that in order to make good secure use of JWT, considerable complexity on both stacks must be considered. Alternatives are either insecure, or not utilizing the benefits of JWT, which would then just be better off using bearer tokens.

Again, thanks for the great article. It really got me thinking about these things and I think a great discussion could be made about the topic.

What is your take on splitting the token into two cookies? Does the added complexity justify the security gained?

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Marko, Putri here – Michelle's cofounder.

That's an interesting suggestion! I don't quite understand how the frontend would miss being able to read the claims/custom data in the JWT using option 3. By storing the access token in memory, you can decode and read the claims in the frontend whenever the access token is available. When the access token is not available in memory (after a refresh/change tab), you can use a function that will refresh the access token, and now you have the access token available again in memory and you can read/decode it in the frontend.

Splitting the JWT might be a useful option if the above solution doesn't help. Let me know what you think :)

Collapse
 
krukru profile image
Marko Kruljac

By storing the token in memory, you risk compromising it by means of xss. The damage is contained since the token is short-lived, but still a window of opportunity exists.
We can either accept this risk or add considerable complexity to reduce it. What do you think?

Thread Thread
 
putrikarunia profile image
Putri Karunia

That's true, storing in memory is still prone to XSS attack, it's just harder for the attacker to find it than localStorage.

Splitting the JWT into 2 cookies where the signature is in an httpOnly cookie, but the rest of the JWT is accessible to JavaScript makes sense. This means that the frontend can still access JWT except for the signature.

I think it's up to the website to determine what kind of attack factor that they're trying to mitigate against to decide whether they need the upgrade in security.

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
 
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
 
ponyjackal profile image
ponyjackal

Hi, I am so excited about this article,
But what if the refresh token takes more than 4KB?
Is there any way to increase the space of Cookie?
Cookie is reling on the type of Browser?

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Pony, refresh tokens are usually opaque random strings stored in your database, so they shouldn't take more than 4KB.

I don't think that there's a way to increase the space, but you might be able to split a large cookie into 2. However some browser limits cookie size per domain, so that wouldn't work.

Here's a nice list about cookie limits per browser browsercookielimits.squawky.net/.

Collapse
 
ponyjackal profile image
ponyjackal

Thank you for your kind support
Love to wait for your next post

Collapse
 
mellen profile image
Matt Ellen

Hi Putri,

Just to let you know that the link in your reply is now dead.

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
 
ezemans profile image
Ezequiel Mansilla

You can use JWT localstorage and prevent CSRF attacks. When you are using a token bearer you are saying to the server that you only allow request with this token from the current browser client, so if a hacker stole the token, he can't make the request because the token are not coming from the original client. JWT is secure and for more security just config the life time of the token less than 8 hours.

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