How to securely store JWT tokens.
In the last years, JWT tokens are widely used as an authentication and authorization method for web applications. They allow backend developers to authenticate users, without making a single query to the database server or any other type of storage. They are super easy to use and they also use the most common format currently used for data on the Internet, JSON.
Because of these facts, there is an increasing number of cases where users store JWT tokens in the wrong way, making web applications vulnerable to different kinds of attacks.
But first, what is a JWT token?
JWT is an abbreviation for JSON Web Token. JWTs are nothing more than a cryptographically signed, base64 representation of a JSON object. By signing the token, we make sure that its content was not altered in any way. This is achieved by verifying the received token with the exact same key that was used to sign it in the first place. In case the signature that we generate does not match the one in the token, we should consider that the token is invalid.
JWT tokens have three parts, all represented as base64 strings:
- A header that usually contains the token’s expiration date, the algorithm used for signing, and extra metadata.
- A JSON payload.
- A signature created by signing the header and the payload
The header and payload are stored in JSON format before signed. The final token is a concatenation of the base64 data of the above, delimited by a period. So, a JWT token would look like the following:
Now, let’s explore which is the best way to store a JWT token.
Should I store my JWT in local storage?
So the answer to this question is: No, never store a JWT in local storage.
But what about session storage?
So again, the answer is the same: Never store a JWT in session storage.
Ok, but I can always use the browser’s memory, right?
Sure, if you make your token available only by your code, that’s a secure solution. The only caveat is that when the user refreshes the browser, she has to log in again. Not so cool right?
I recommend using this approach when the user, for some unknown reason, MUST log in again when refreshing the browser,
But if I cannot use any of these then what? Cookies? Cookies are so 2010 …
Stop dissing cookies, cookies are great. We have been using them for years, they are automatically sent in every browser request (including ajax if we want them to) and they are SUPER SECURE!!!
That’s true, but only if the server doesn’t set the HttpOnly flag, something you should always set for authentication or authorization cookies. So distant voice, are we ok with that?
Distant voice: Of course not, what if someone sents them through a non-secure HTTP request?
First of all, nobody should use plain HTTP these days, but even this way, we can set the Secure flag when creating the cookie, so it will be never sent through a non-secure connection. I think we can move on…
Distant voice: Are you in a hurry bro? You forgot CSRF.
F@!k. It’s true, I forgot CSRF attacks. So, what is a CSRF attack?
A CSRF attack is performed when an attacker takes advantage of the browser’s default behavior, to send all cookies even on cross-domain requests. This can lead to great security vulnerabilities if not handled correctly. An example of this attack is the following:
- The attacker sends an email with a beautiful offer, and adds a CTA button at the end, “GET A 50% DISCOUNT”.
- The user is thrilled by this awesome offer and clicks the button.
- In reality, the button submits a POST form to your web application, and more specifically to the endpoint that changes the user’s password with a new one.
- Now the user is logged out and cannot log in to your web application anymore.
We have to mention that this attack would be performed by an attacker that just wants to play a little bit and test its skills. More serious operations could have been performed like making the user upgrade to a very expensive plan, or even transferring money from their bank account to the attacker’s account.
So what can I do about it?
The first option is to just forget about it, never tell anyone, and hope that nobody ever finds out (especially a malicious hacker). You are ready to go!!! Just kidding…
You can use a backend generated token in every request that you perform to the server (usually any POST, PUT, DELETE request), so when the user performs the request, you check if the token is valid by fetching it from some cache or even directly from the database. But this requires that you create a new token every the user redirects to a new page? So is there an alternative?
Each of them is useful in its own case.
You can find browser compatibility here.
So what is Lax? Lax allows cookies to be sent in cross-domain requests if the request verb is GET. All other requests will not contain cookies with the Lax SameSite policy. This way, it allows cookies to be used when, for example, redirecting the user from an email to a dashboard screen of your app, but don’t allow a malicious form post data to a sensitive endpoint.
We have to mention that Lax works for all redirect GET requests. It won’t sent cookies in ajax request, so nobody can just add some code in their site and request, for example, the /me endpoint, stealing the logged-in user personal data. It will be also ignored in the case of iframes, even for redirect requests.
So the most usual ways a Lax cookie is sent from a cross-domain GET request are the following.
- A user clicks a link that redirects to your website
- A user submits a GET form request that redirects to your website (useful in case you want to provide a way to pass dynamic arguments to the query string using a form, from another site)
- If a command like window.location.href=”yoursite…” is used.
Now, Strict SameSite policy will not allow the cookie to be passed through cross-domain requests in any way. This can be useful in some edge cases, but you have to understand that even a simple link redirect will leave your user logged out from your web application if you use this policy. Of course, it’s the safest one.
So, I just use Lax or Strict, and I can now sleep without the fear of a CSRF or XSS attack. Right?
Almost. You have to make sure that your user is using a modern browser that supports this kind of functionality. In case they don’t, the best thing to do is to inform the user that your application is not currently supported by the specific browser version and that they should upgrade to a newer version.
In case you don’t use a message like this, there is the possibility that the server will try to set the SameSite policy, but the browser is going to ignore it. For example, Internet Explorer does not support this kind of functionality. If you need explicit support for an older browser, you should fallback to the CSRF token implementation.
If you found this blog post useful, you can subscribe to my newsletter and get to know first about any new posts.
I use a simpler approach:
When a request is made, the server reads the claims from an Authorization header (set by client app), the signature from the cookie and rebuilds it for validation. This way we can protect against most kind of attacks.
Very interesting approach. I will definitely try it in my next project.
How do you handle if user delete localStorage?
If the user manually deletes its browser local storage content it’s their own choice and will affect the application as it will lose a part of the JWT token and the backend will not be able to verify it, so the client will have to login again I guess, based on what Pedro said in the previous comment. In case the user fully disables local storage they won’t be able to use the application at all. Is this what you were asking?
Yea that was my question, so automatic logout will happen, hm.. ok that is fair. Thanks, fo the answer.
Btw. I like the approach of moving JWT to cookies.
The only thing that bugs me is if you have FE app with its own routing, how would you be able to tell difference if user is logged in or not without pinging be API.
I guess exposing part of JWT to the user and saving it to localStorage, and part of it passing with cookies would do the trick.
That is right. If you prefer the approach of storing the whole jwt in just one cookie without storing the signature in localstorage, you will find that the jwt has expired in the first request that returns a 401 status code, so you ‘ll have to login again, or request a new token in case it expired because it’s a short lived one.
Auth0 is using iframes to securely persist tokens. How this compares to using cookies? https://github.com/auth0/auth0-spa-js
Auth0-spa-js uses in-memory storage or local storage. The iframe is used as a fallback, in case the module cannot find a token or refresh token anywhere (local storage or memory). Then, it uses the iframe to get a new token using the Auth0 session that is stored inside a cookie. If I get it right, the iframe is used to retrieve a new token and refresh token when the page is refreshed, as there is already a session cookie in the iframe, but the original tokens have been lost. Also, when using the technique that I described in the article,… Read more »