Update August 1st, 2022
I recently had a new hot take on this subject on YouTube, check it out:
Some time ago, I had to store the JWT token returned from Strapi in a React web app. Strapi is a stateless server. The React app is client-side. Here's what the internet has to say when I need to learn "jwt storage":
Seems like most opposed to the idea of using
What about "react jwt storage"?
First one that recommends
localStorage! Hmm, there are also people that
recommend storing JWT in Cookie. Also, there's a recommendation of using
in-memory storage. It's not persistent. I don't want to login every page
refreshes. So let's not talk about it.
Let's dig into the first StackOverflow post above:
Refresh Token should be stored in a cookie with
httpOnly flag. Hold that idea
for a second.
How about the second post:
Interesting. Two posts that recommend the same Cookie approach. Look at the 3rd
point. They said that I should store my JWT in a Cookie, because in point number
localStorage is exposed to XSS. Is Cookie immune to XSS? What about
CSRF? My server is stateless, and as you read on this blog post further,
you'll need to implement the Double Submit Cookie CSRF prevention technique
(I'm using stateless server, remember), which is NOT immune to XSS,
because the CSRF token also needs to be present in the request body. Not to
mention that now you have to prevent both XSS and CSRF. Hence the 3rd point is
not valid, in my opinion, just because of the XSS argument alone.
You can see the problem here. Back then, I didn't knowledgeable enough to know that storing JWT in Cookie have problems. It's easy to blindly listen to these SO answers when you're starting out as an intern (Yes, I had to implement this in my internship). Obviously, this needs to be addressed. If someone is in the same boot as me trying to get into the new Serverless world, they need to know the reasoning behind these.
Original post starts here:
You know the drill. Authentication time, you chuck the access token into your
localStorage. Suddenly you have a flashback of bad luck working with
localStorage, namely they could easily be taken by a script via Cross-Site
Scripting. So you turned to
httpOnly cookie (along with some other attributes,
stay tuned). You even went as far as using a refresh token, and implement a CSRF
token thing. Solved, right?
You missed a point about cookies
Whenever you touch a cookie, you are blessed with a new problem: Cross-Site Request Forgery a.k.a. CSRF. Take this from OWASP Cheat Sheet of CSRF Prevention, I'll provide an example later.
Let's switch gears here. I'm gonna implement a CSRF mitigation. According to the Cheat Sheet, I have to implement something called a Double Submit Cookie (I'm cutting corners here, we are in a stateless development age, there's no server state, just in case you wonder why not the Synchronizer Token Pattern).
DISCLAIMER: This is simplified. I'm sure there's more sophisticated ways to perform the CSRF attack.
I'm at the Cheat Sheet. Here's the steps:
- On the server side, I would have to generate a pseudorandom value and put them in a cookie.
- Every time I need to send out a request, I would put that value (also called
a CSRF token) into a header field, something along the lines of
X-XSRF-Token. The Cookie from step 1 is sent along with the request.
- On the server side again, I would have to implement a comparison between those two.
Here's a visualization of what's going on:
The end result is, if the attacker wants to do the CSRF now, they would need to:
- Set a freshly dummy value, replacing the CSRF token in the request
body/header. Easy enough. For example:
POST /user/transfer X-CSRF-Token: MY_DUMMY_VALUE
- Modify the
MY_DUMMY_VALUE. Not trivial, but doable.
POST /user/transfer Cookie: csrfToken=MY_DUMMY_VALUE X-CSRF-Token: MY_DUMMY_VALUE
- This request is placed within a malicious site, e.g.
SameSiteflag and the browser's implementation, the
accessTokenCookie would be sent.
Okay, it's pretty good if combined with some
SameSite or some other things. CSRF solved.
Now you go read this document from OWASP and find out that your CSRF prevention
attempt is flawed. Then, hopefully not, read this from StackOverflow if
you store the csrfToken not in the cookie, but in localStorage. If you go as
far as using
SameSite=Strict setting, remember that Setting a cookie as
Strict can affect browsing experience negatively, meaning a setting of
SameSite=Lax still leaves top-level
If that's not enough, not all APIs are equipped with CSRF prevention framework. Google "Strapi CSRF" and you'll basically find nothing besides of implementing CSRF prevention yourself. Why? Because Strapi is not setting the token in a cookie.
However, assuming that CSRF is absolutely prevented, there's still the elephant in the room, the absolute CSRF prevention buster. Read on.
Here's my take on it.
See, you still have to put the CSRF token in the body, by design. That means
your CSRF token cannot be
reading the cookie. Have you realized yet? You've just achieved Cross-Site
Here it is again:
If you say
But it makes the access token harder for the attacker to get than localStorage! If not they'd just snatch it!
So what? Take this, your website has a Cross-Site Scripting problem, not CSRF.
Do you sleep well on that? You have just gone full circle.
XSS problem, but cookie has CSRF and XSS. That's why I'm not eating the
Side note: About the Refresh Token, it solves nothing. It is still a token.
Store it in the Cookie and you have CSRF and XSS, just like a normal token.
You can't use
httpOnly if you want to prevent CSRF.
Fixing the myth of localStorage by mending XSS
Let's debunk this. You got the XSS problem. Deal with it. There's a reason why
So where are you getting these foreign scripts to run on your site? Let's start
with some of the greatest archnemesis of
XSS from Node libraries
I'm talking about your
node_modules when you're deploying on Vercel or
something. Think about it. There are literally from hundreds to millions of
projects using that very same library. If that happens, you would know. You
would definitely know.
Here's something from GitLab to let them check for your development comfort.
Or just run
XSS from CDN libraries
Alright, here's the interesting part. CDNs can be compromised. Why do you use them though? Just use NPM. And how do you use TypeScript with CDN anyway? Why are your libraries not available in NPM? If it's that niche, why not just write them yourselves if you're literally 1 out of the 3 people that would use those? Why do I have so many questions for you?
If you're using libraries from CDNs I think you're having a bigger problem.
XSS from JS injection
Also called a Stored XSS. Or any JS injection from form inputs. It's pretty hard to screw up something that would cause XSS without you knowing first-hand. Take a look at this StackOverflow on why React has some XSS-proof. Make sure to make use of some ESLint rules, those are pretty handy.
In the nutshell, it's pretty hard these days to get XSS if you're doing things right.
localStorage. I just stripped out a workday of effort in your life by doing
so. There's nothing about that anymore, right? The stereotypical thing about
localStorage is, they have XSS problem, and they do, just as your CSRF
mitigation. Don't overengineer your system.
Here's something you'll dig: Why avoiding LocalStorage for tokens is the wrong solution