This question is a sensitive subject all over the internet. Wherever you'll look at, peoples tend to be really dogmatic.
- Don't store it in Local Storage !!! Don't you know about XSS attacks ?!
- Please stop believing that storing your JWT in an HttpOnly cookie is secure........ You're still exposed to XSRF attacks.
You get the idea.
Long story short, I was looking for informations to build a robust authentication system myself. I did know nothing about the attacks quoted above, and of course, how to protect my application.
I'll do my best to summarize what I learnt, what are the different techniques and their fallbacks. This article will also try to be as opinion free as possible.
Without further ado, let's dive in.
Disclaimer: I'll purposely fly through what a JWT is, to focus on the security part. You may find information on their dedicated website.
Because there's one.
Let's assume that you're building a new website, and you're on the authentication part. After some research, you find out that the go-to (as time of writing this) is using a JWT, a Json Web Token.
A JWT is basically an encoded string that will contain some basic informations (anything you want). Your server will send it back to you when you'll do your login process, and your client will need to supply it on any further requests where authentication is needed in order to be accepted by the server.
In short, a JWT is a way to identify your user as a legitimate and authenticated one towards your server.
So .. If we need to supply the JWT on any further request that need authentication, where do we tore it ?
This is where things get interesting.
My first idea, like a lot of people I believe, was to store my newly obtained JWT in the browser Local Storage. Things would be as simple as :
And whenever we need it back :
Despite beign the easiest way to store our JWT, it turns out that this is, by far, the most insecure way.
Cross Site Scripting
Basically, an XSS attack happen when some undesirable code is being executed within your website. This can be as gentle as a console.log, but could go as far as stealing informations, our JWT for example.
Let's just take a very contrived example to understand it better.
Pretty simple, right ? Now here's the catch, what is sent through the form is not being sanitized (meaning any unsecured or unrelated part of the data is removed or escaped), and so an hacker could insert a harmful script.
<div> I juste created an amazing blog post !! <script>functionToReadYourJWTandSendItToMe()</script> Please, accept it ! </div>
This get inserted into the database, and when the admin open is page to see the preview of the blog post, the script will be hidden and being executed, succesfully stealing the admin JWT !
And if the admin accept the blog post, and it get displayed on the website homepage, the script will execute for every visitor that will open the page .. Stealing everyone JWT !
Here's a recap :
Storing the JWT in localStorage without the proper defences against XSS can be dramatic, this left the hacker with a potential large area of actions all over your website to try to find a breach.
The developpers now have the responsability to check for every possible breach and be mindful when developing new features.
There are ways to secure our app to XSS, such as sanitizing everything that would go into the database.
An easy to implement, but somewhat risky solution.
While digging further in order to find informations about localStorage, I've seen a lot of peoples recommending to store the JWT into an HttpOnly Cookie. If you're not confident what a cookie is, feel free to go to the MDN documentation.
Be mindful that the HttpOnly part is the most important one. A cookie without the HttpOnly attribute could be read by some JS code, sending us back to the XSS problem.
By applying the attribute, we restrict the use of this cookie for HTTP requests only, securing us completely from XSS.
But .. We're now prone to XSRF attacks.
Cross Site Request Forgery
As the name may imply, the goal of this attack is to create a request on a malicious website to be executed on the targeted website. Let's take a real world example to understand it better.
You have your website open and you're logged in. Your JWT is securely stored into an HttpOnly cookie, meaning that every request you send to your server will automatically include the cookie, and so your JWT.
As every application with a user account, you have the possibility to change some informations by filling a form. This will send to your server a request, it'll verify your JWT, and allow the changes.
As you navigate to it, you received an email. You open a new tab, open the email and click on the link.
☠️ The website you lend on have a script that execute as soon as you open the page. Prepared in advance, it execute a request on your website. ☠️
How ? Well, the hacker could have created an account, open the dev tools and saw what was the endpoint to your server.
Basically the hacker send the same request as you would done, but he control the informations. Your username has been changed, your profile picture aswell .. Maybe even your password.
The most amazing part about this attack is that the hacker doesn't have to recover the JWT, it's automatically included within the HTTP request.
There are ways to secure your website from such attacks, which we won't cover here, but most of them tend to be prone to .. XSS.
Maybe even a simplier solution than localStorage, the goal is fairly simple. You attribute the JWT to a variable, and make it available for your needs.
const jwt = ...;
This variable is impossible to reach for a hacker, neither from an XSS or XSRF attacks.
Such a simple solution as one serious downside : whenever your user will close your website, the next time he'll come back, he'll need to login again, creating a very poor user experience.
Just like the other solutions, there are ways to mitigate the downsides.
When you request your initial JWT, the plan is to get an extra token, a refresh_token token (which is basically a JWT that will live longer). This token will be saved in the browser within an HttpOnly cookie, aswell as on the server within a database. His goal is to keep the user login without him having to go through the login process everytime your JWT expire, such a process is called a silent refresh.
We can actually use this behavior to pretend the user session is being persisted. As the refresh_token is stored within the cookies, we can use it across sessions. When our website boot-up, we will trigger a call to a particular endpoint, this endpoint will return a JWT only if the refresh_token is still valid.
- How is this secure if the refresh_token is a JWT too ?
The refresh_token will only be used and accepted in the particular endpoint that is dedicated to him. Trying to access the rest of the API with it will fail.
- But a hacker could use a XSRF, right ?
Yes but he won't be able to see the JWT that is returned.
This method leads to a lot of boilerplate and overhead.
None of the solutions above are bullet-proof, there's always a way for a brilliant attacker to get in. Some solutions are easier to implement, some require more setup but offer an arguably better overall "protection".
Pick what suit you the best.
I hope it helped you to understand this incredibly dense topic as much as I did writing this.