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
andsecure
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
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:
- What On Earth Is OAuth? A Super Simple Intro to OAuth 2.0, Access Tokens, and How to Implement it in your Site
- Passwordless Login with Email and JSON Web Token (JWT) Authentication using Next.js
- Here's How to Integrate Cotter's Magic Link to Your Webflow Site in Less Than 15 minutes!
References
We referred to several articles when writing this blog, especially from these articles:
- Please Stop Using Local Storage
- The Ultimate Guide to handling JWTs on front-end clients (GraphQL)
- Cookies vs Localstorage for sessions – everything you need to know
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 (45)
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.
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.)
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.
Hey thanks for the response! Best wishes.
Great article. Thanks for the in depth research and clear tutorial. Logic was very concise. 😃
Happy to help! Feel free to ping me if you have any questions/concerns :)
Thanks
Was in a long search for this clarification.
Thanks
Thanks Anshul! Let me know if you want me to discuss any other topics related to Authentication :)
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.
Thanks for this article, it helped me a lot!
Thanks Lucien! Let me know if you have any questions :)
If you use Express, then it could be worth looking at Express Session and the option to save the data to Redis:
Yes, redis is the best one🙂, also cookies would be my second option for JWT based storage
Hi Wayne, Putri here – Michelle's cofounder.
This is very helpful, Express Session with Redis is definitely a great option. Thanks for the comment!
A pleasure, and glad to help.
Very descriptive and helpful article.
Thanks!!!
Thanks Jorge!
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?
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 :)
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?
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.
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.
Hi Pacharapol!
Cookies that are marked
httpOnly
are not accessible fromdocument.cookie
, otherwise you can access the cookie fromdocument.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:and it will:
If you're interested, shoot me a message on Slack and I can help you with any questions. You can find our documentation here.
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?
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?
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/.
Thank you for your kind support
Love to wait for your next post
Hi Putri,
Just to let you know that the link in your reply is now dead.
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.
Surely the vulnerability here is that you have a site vulnerable to XSS not the choice of where to store the token?
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.
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?
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.
Very nice article, it's a good read.
I've a question, if i submit a
/refresh_token
request in the attack code, can I get the user's access token?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.