DEV Community

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

LocalStorage vs Cookies: All You Need To Know About Storing JWT Tokens Securely in The Front-End

Michelle Wirantono on July 21, 2020

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

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.

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
 
sincness profile image
Maltheboss

Very nice article, it's a good read.

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

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

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
 
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
 
dorneanu profile image
Victor Dorneanu

Straight to the point! Thanks!