DEV Community

Cover image for How to securely store JWT tokens.
George Koniaris
George Koniaris

Posted on • Edited on • Originally published at gkoniaris.gr

How to securely store JWT tokens.

Article originally published here.

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:

[header].[payload].[signature]

Now, let’s explore which is the best way to store a JWT token.

Should I store my JWT in local storage?

Most people tend to store their JWTs in the local storage of the web browser. This tactic leaves your applications open to an attack called XSS. We will only discuss XSS in the JWT context, you can find more about it here. In this kind of attack, an attacker takes advantage of the fact that local storage is accessible by any javascript code running on the same domain that the web applications hosted. So, for example, if the attacker can find a way to inject maliciously javascript code inside your application (by injecting the code in a node module that you use without knowing about it), your JWT token is immediately available to their.

So the answer to this question is: No, never store a JWT in local storage.

But what about session storage?

Hmm, let’s see what happens in this case. Like local storage, session storage is accessible by any javascript code running on the same domain that the web application is hosted. So the only thing that changes, is that when a user closes their browser, the JWT will disappear and the user will have to login again in their next visit to your web application.

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, they have 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!!!

Distant voice: But cookies are also accessible through javascript.

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.

  • Because cookies are sent in every request, even cross-domain ones, the endpoint works as expected if the user has logged in, in an earlier step to your web application.

  • 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 time the user redirects to a new page? So is there an alternative?

For many years, unfortunately, the only way to be safe was to use a CSRF token when using cookie-based authentication. From 2016, modern browsers started implementing a cookie policy called SameSite. SameSite can take one of the following three values:

Each of them is useful in its own case.

The first one, None, allow the cookie to be sent in every possible request, including cross-domain. This is how browsers were treating cookies for many years. The new default value for SameSite of most modern browsers is Lax, although not all browsers are using Lax as the default SameSite cookie policy yet.

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 users are 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.

Background vector created by vectorpocket – www.freepik.com

Top comments (53)

Collapse
 
inkeliz_35 profile image
Lucas Rodrigues • Edited

If you use CSP to block any inline-script/insecure-script and also enforce that only trusted JS files must be accepted: there's no issue about using LocalStorage.

The article doesn't make sense for me. Yes, using cookies might make your session-value secure. But, let's suppose that your website is vulnerable by XSS in the first place. The attacker can send a request using that session, without the need to steal the session value itself. The "CSRF protection" doesn't protect against XSS, because the requests are executed inside your own website.

Collapse
 
gkoniaris profile image
George Koniaris

Hello Lucas. I agree that this way you would be protected by scripts that are not hosted in your own domain. But in case there is, for example, a node module that you installed while developing it would be considered insecure. I understand your point and it is 100% valid. But with this approach, even in the rare case that a module was vulnerable and you had by accident injected it in your base app, the module could never send the token to an external domain through an Ajax request. It could only hit your internal API, but this would require that the attacker had injected malicious code in a node module just to attack your web application specifically. Let me know about your thoughts.

Collapse
 
inkeliz_35 profile image
Lucas Rodrigues • Edited

I don't know. That issue about leak the session can also be fixed with CSP, since you can block external communications too. I never use node modules, and you might guess why, so I can't say anything about it.

What I'm saying is that it is easy to find solutions when you have such a small attack surface, and never tell it. You can also suggest encrypting the local storage value using a random key using, with a random algorithm with a random name... Yes, attackers can extract the key (...), but then use the same argument: "just to attack your web application specifically". The opposite scenario might be valid: since I'm considering that the page is secure against XSS, then I can use LocalStorage.

I think it would be better if it compares all alternatives (SessionStorage, LocalStorage, Cookies, Credential Management API, IndexedDB API) and all kinds of known attacks.

Thread Thread
 
gkoniaris profile image
George Koniaris • Edited

So you suggest serving external js from another domain and set a CSP to disallow these scripts to access local storage or make ajax requests?

That’s correct. But if you serve external scripts through a CDN for example, and set a csp you are secure with the cookie implementation too. I may write a complementary article or extend this one at some point. Thanks for your feedback its really valuable!!!

Collapse
 
asdftd profile image
Milebroke

That is not true. With javascript you can just create a tag with an src to the attacker sites - and as we know the browser always sends the cookie with the requests - so bingo the attacker now has your cookie. Please beware of that

Thread Thread
 
gkoniaris profile image
George Koniaris

Hey Milebroke,

Not sure which comment you wanted to leave a reply. If you refer to cookies, in the case of Lax and Strict they won't be sent to the attacker's website if we, for example, inject an image with the attackers URL as the source. That's because it's not considered a first level navigation event.

Although, it may be sent if the attacker creates a GET form and sends it by clicking the button through javascript. I have to admit that I have not tested this scenario, but you will be fine if you just use Strict flag, as it will prevent all cross domain cookie sharing.

Collapse
 
gkoniaris profile image
George Koniaris • Edited

Also a small comment, I personally think that the endpoints that are related to payments and generally sensitive account actions should require an extra login with a different authentication method, valid for one API call only (especially for payment API calls) and should be served in a page where only vanilla javascript written by the web application developers is used.

Collapse
 
itswillt profile image
Will T.

Just some thought, but isn't the method of using Cookies just as vulnerable to XSS as that of using localStorage? I'm new to Web Security and have been wondering this all the time. I mean if an XSS attack happens, the attacker can do whatever they want on behalf of the victim, including maliciously sending requests that have totally valid cookies. The cookie-based authentication, from my understanding, can at best introduce a little bit of inconvenience for the attacker, but at the same time making the authentication process much more complex. Therefore, in the end, what you have to do is to make sure you're not (so) vulnerable to XSS or it is your doom.
Nice post btw, especially the part about SameSite policy stuff, very informative.

Collapse
 
gkoniaris profile image
George Koniaris • Edited

Hello there,

Thanks for commenting. Cookies are XSS vulnerable in the case that we don’t set an HttpOnly flag in the server when creating the cookie. When the cookie is set with an HttpOnly flag it can’t be accessed by javascript in any way (at least in modern browsers, I don’t know what happens with ie 7 or 8 :p)

Collapse
 
inkeliz_35 profile image
Lucas Rodrigues • Edited

I think @quochuytlbk does not suppose that you want to steal the session, but use the session already active on the page. Let's assume that your website is vulnerable by XSS, then I can inject something like: fetch('api/account', {method: 'DELETE'}). I still don't know your session/cookie, but I manage to use them to delete your account. The fetch call was performed inside your website, it's not CSRF.

Thread Thread
 
gkoniaris profile image
George Koniaris • Edited

Nothing is 100% secure, but that would require that the attacker finds a way to inject code to specifically target my own web application. What you say is 100% valid, my approach is vulnerable to this kind of attack. But it would also be vulnerable if someone was using local storage. I don't know if there is any way that you can prevent this type of attack.

Thread Thread
 
itswillt profile image
Will T. • Edited

Lucas delivered what I failed to. So yes, that's the point. When an XSS vulnerability is exploited, nothing can save our users. The best bet is to prevent XSS from happening in the first place, or everything is vulnerable.
That being said, why would I go through all the trouble of trying to protect cookies from both CSRF and XSS, just to introduce a little bit of inconvenience to the attacker? At least with localStorage, I don't have to care about protecting against CSRF.
So in my opinion, using XSS as an exclusive drawback of localStorage can be misleading. When I first started to dig into the "localStorage vs Cookies" battle, I unknowingly thought it was "Protecting against XSS vs Protecting against CSRF". That is of course not true. It is "Protecting against XSS vs Protecting against CSRF and XSS".
I think one of the valid use cases of cookies over localStorage (or sessionStorage, IndexedDB, etc.) is SSR (where there is no localStorage).
Edit: Another valid use case of cookies is when you have subdomains because I've heard that localStorage doesn't work across subdomains.

Thread Thread
 
itswillt profile image
Will T.

So I have come to my own conclusion that it all depends on your use cases and how you use the methods. There is no "localStorage is totally better than cookies" or vice versa.

Collapse
 
franky47 profile image
François Best

A technique I use is to split the JWT into two cookies. The header + payload accessible through JavaScript (for client-side reading of the claims), and the signature is HTTP only (not accessible through JavaScript). Align cookie expiration with JWT expiration for auto-logout.

medium.com/lightrail/getting-token...

Collapse
 
charlesgiroux profile image
Charles-Antoine Giroux

I use this.

Header + Payload are stored in LocalStorage and sent in a header with fetch.
Signature is in a cookie with HttpOnly.

The server stitches the header with the cookie and then validates the JWT. This works well in IE 11 which does not support SameSite cookies.

Collapse
 
gkoniaris profile image
George Koniaris

Nice approach

Collapse
 
lukehglazebrook profile image
Luke Glazebrook

First off, great article! I've recently finished building an authentication system that works very similar to the one you've described but, with a distinct difference: I store the JWT across two cookies.

One cookie contains just the JWT header and payload and can be accessed by JavaScript, the other contains the signature but is Secure + HttpOnly. This way I can access the payload on the client without worrying about having the entire token potentially compromised.

Authentication in SPA the right way by Jean-Cristophe Baey describes this approach in a bit more detail.

Collapse
 
gkoniaris profile image
George Koniaris

Thank you very much. That's a very clever approach!!! I will definitely try it on my next projects.

Collapse
 
anduser96 profile image
Andrei Gatej

Interesting approach. But would it be wrong if you'd read the payload from a HTTP response instead? IMO, it would be the same thing as reading it from the cookie.

Collapse
 
lt0mm profile image
Tom

first of all it's a really great post! But I have one question in case for example you have separate authentication server you can not use HttpOnly flag right? or in that case I should use just subdomain?

Collapse
 
gkoniaris profile image
George Koniaris

Can you give a specific example of how you authenticate (the login process) and then how the other APIS validate that the user is authenticated?

Collapse
 
lt0mm profile image
Tom

I was some time ago already so I don't remember all details to be honest, but the gist is frontend redirected user to authentication server (we used keycloak) then we stored token in the local storage and we reissued it like each minute (or something like that) as I remember. Backend used keycloak adapter which again made request to keycloak server to validate the token.

Thread Thread
 
gkoniaris profile image
George Koniaris

OK, so I guess that backend was also making a request to keycloak (through redirect) and was then returning the token to the user. If that's the case then you just have to set the token in a Secure HttpOnly Strict cookie instead of just returning it, for example as JSON and saving it to the local storage.

Thread Thread
 
lt0mm profile image
Tom • Edited

hm not exactly authentication happened exactly on keycloak server and then backend made request to keycloak just to validate the token, so user received token from keycloak server and there for I suppose just cookies solution would not work. As I understand that solution could be migrated from local storate to cookies only if we moved keycloak to some subdomain and used subdomain cookies sharing. But definitely I can confuse something

Thread Thread
 
gkoniaris profile image
George Koniaris

If it follows the oauth2 flow, you could redirect to the backend of your web application instead of the browser, so then the backend would set the cookie and would redirect back to the frontend page.

Thread Thread
 
lt0mm profile image
Tom

yeah it would work I think thank you for the answer!!

Collapse
 
sabberworm profile image
Raphael Schweikert • Edited

[cookies] are automatically sent in every browser request […]

Therein exactly lies the reason why people tend to shy away from them. It means blowing up every request by a few (or, in case of, JWTs, many) bytes, even requests that don’t require authentication like images, CSS, scripts…

Of course you could work around that by using a different domain for static assets or by using a single path prefix for all requests that need authentication and then set the Path flag of the cookie to that value but all of these things require infrastructure changes…

Collapse
 
gkoniaris profile image
George Koniaris

Hi Raphael,

Thanks for mentioning this issue. It may not seem like a big deal but in some cases it is. Personally, I try to use cookie-free domains if speed is an important factor for my applications.
I remember reading an article talking about people lining in Africa, who could not even access simple pages because of the tons of modules, CSS and javascript that we use on our websites today. It's pretty sad if you think about it, a big part of today's world doesn't have access to the internet because we don't make our own web applications accessible to non-broadband connections.

Collapse
 
kataras profile image
Gerasimos (Makis) Maropoulos

Nice post George, I've recently integrated the SameSite cookie policy to one of my Go packages, the iris web framework one (github.com/kataras/iris) and users loved it so I totally encourage web developers to set a cookie policy through SameSite, it's fairly easy-to-understand concept and it looks like 2020

Collapse
 
titi profile image
Thibault ROHMER • Edited

One thing missing (from most web site authent. tutorials) is: how many tokens/sessions are currently allowed for a single user?
Because whatever the method [in your post], this does not prevent a whole range of issues.
Let's imagine you lend me your computer so that i can reserve a train, but instead go to DevTools and copy the cookie/localStorage/sessionStorage, i'm done in less than 30s and i can reuse the token whenever i want on my computer :)

If you want more security, you need to store a list of currently allowed signatures on server side.
This way you can:

  • check if user is already logged from somewhere else (physical location or other browser for instance)
  • restrict number of simultaneous sessions for a user
  • provide a way to revoke tokens to the user. See for instance in gmail : "Open in x other location · Details" and "Sign out all other Gmail web sessions" button

Note : do not store the whole token, otherwise if your database is compromised, tokens can be used to impersonate anyone...

This can be also completed with:

  • fingerprinting of the device (user agent, extensions...) to trust only some devices and trigger an alert for any untrusted device.
  • brute force prevention if someone tried to login but failed, take exponentially more time to respond (up to a few seconds) this requires thinking when scaling your web app on multiples nodes (ex: keep number of tentatives in a redis instances)
Collapse
 
gkoniaris profile image
George Koniaris

Hey Thibault,

Thanks for sharing all this info. All these things are really useful. Of course, they cannot be covered in a single tutorial, and it would also be out of the scope of the article.

I haven't implemented a solution that is so complex till now. Do you think that JWT tokens are a good solution for those cases? I mean, if you need to perform actions like checking if a user is logged in, JWT automatically loses a big advantage compared to traditional sessions, the ability to authenticate the user without any query to a DB server. Of course, you keep the authentication stateless but this can be achieved with generated ids that identify the user session (stateless session) too. What's your opinion on these subjects?

Collapse
 
titi profile image
Thibault ROHMER

Correct.
I'm not experienced enough to give you a definitive answer.

Managing traditional sessions isn't that simple either, especially when you want the "remember me" feature. By implementing things yourself, you're likely to open doors.
Thus, a JWT library with stateless session definitely brings advantages. I also like the encoded payload+signature compared to a lot of things seen in cookies :p

So maybe using JWT and storing only the signature in a table with a foreign key on the user would be interesting.

  • Checks are only made during authentication.
  • Every other http call is stateless
Collapse
 
maxymapp profile image
Maksym Kulikovskiy

"verifying the received token with the exact same key that was used to sign it in the first place" -- to be precise, a private key is used to sign the token and a public key to verify the authenticity of the token.

Collapse
 
gkoniaris profile image
George Koniaris

Hi there,

Thank you for commenting!

The way you proposed to perform the signing and validation of the token is also valid but its not the only one. JWT tokens can be signed using HMAC where only a private key is used to sign and verify the token. This is used in most cases where only the backend needs to verify the token, and the frontend just needs to decode it (everyone can decode a jwt as its just a base64 representation of our data).

If you sign JWT tokens and you need your frontend to be able to verify that it was signed by a valid authority, you can use a private key to sign on the backend, and provide the public key to everyone to be able to verify the token.

Is this what you mean?

PS: This article assumes that we use the HMAC way to sign the tokens.

Collapse
 
maxymapp profile image
Maksym Kulikovskiy

That's perfect, thank you for clarifying the use case where HMAC shines.

Collapse
 
masylum profile image
Pau Ramon Revilla

If you are storing your JWT tokens in the cookies because they are secure enough... Why do you need JWT to start with? Just store the data in the cookie, no?

Collapse
 
gkoniaris profile image
George Koniaris

Hi there,

If you store data in a cookie, as JSON for example, how would you validate that the data sent to your server is valid? I mean, everybody could just create a JSON representation of a user, send it through an HTTP cookie and then they would be allowed to performed authorized actions in your web application. But I think that you refer to the case that you want to access the data of the JWT in the frontend.

The examples of these articles assume that you don't need access to the JWT in the frontend (I mean access like getting the full name of the user using the JWT or something like this) and that this kind of JWT is used only for authentication purposes, to make it easier for the backend devs to validate that the user is logged in without performing queries or using sessions in the traditional way. For frontend access to the profile of the user, I prefer to get this data from a /me or /profile endpoint, as it's always up to date and I don't have to mess with decoding JWT in the frontend too.

Does this answer make sense to your question, or did I get something wrong?