DEV Community

loading...
Cover image for How to securely store JWT tokens.

How to securely store JWT tokens.

George Koniaris on April 11, 2020

Article originally published here. In the last years, JWT tokens are widely used as an authentication and authorization method for web application...
pic
Editor guide
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 Author • 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
gkoniaris profile image
George Koniaris Author

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 Author • 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 Author

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
quochuytlbk profile image
Huy Ta Quoc

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 Author • 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 Author • 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
quochuytlbk profile image
Huy Ta Quoc • 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
quochuytlbk profile image
Huy Ta Quoc

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
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 Author

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 Author

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 Author

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
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 Author

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

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 Author

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
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
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 Author

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 Author

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
alexandis profile image
alexandis

We use IdentityServer4 in our .NET solution, which also includes Web API and Angular front-end app. There are tons of middleware settings (it's actually an ABP framework-based solution). At the very end, all authentication tokens are stored in Local Storage (I have not found where exactly it's set up, BTW). Anyway, it has worked somehow in our DEV environment. But suddenly the need to use window.open from Angular app popped up. And it causes a lot of headache: now, to identify user in server page, called from window.open, we need to use cookies (URL is not considered of course). Does it mean we have to switch fully from Local Storage to Cookies Storage? How to set it up? My idea was to copy access_token when it is created (in Local Storage) to Cookies and delete when a user logs off and probably under bunch of different conditions, like browser window is closed, etc. (which ones exactly?) Where to find all related info?

Collapse
kevinhch profile image
Kevin

Theeeen what is the point, should I store the token in cookies with Https and only flag?

Collapse
gkoniaris profile image
George Koniaris Author

If you store the cookie with HttpOnly flag, Secure flat and SameSite=Lax|Strict flag you are secure and good to go! This article assumes that you don't need access to the JWT content in the frontend, so it is ok to use the HTTP flag. So by setting the HttpOnly flag, the cookie can not be retrieved from Javascript, and by setting the SecureSite=Lax|Strict it won't be sent in cross-domain requests. Sorry if I was not 100% clear on this point.

Collapse
kevinhch profile image
Kevin

Ok, but if i need to get access from the frontend to make some request to my API in the backend, how should I get the token to authenticate my API calls from JS?

Thread Thread
gkoniaris profile image
George Koniaris Author

If your frontend is on the same domain as your API (or a first-level subdomain), the cookie should be automatically sent through Ajax requests. For example, if your web application is hosted on kevin.dev and your API is hosted on api.kevin.dev, the cookie will be sent in every Ajax request automatically. Of course, you should make your API fetch the cookie in the backend, and verify the token. In case you need to access the payload of the JWT token, that's not possible with HttpOnly cookies. You should make an extra request to your API, something like a /me endpoint, and save the data in a Javascript variable.

Thread Thread
kevinhch profile image
Kevin

Oh :O thnxs a lot, finally a good post to solve my problem!!

Collapse
kontsedal profile image
Bohdan

Short living jwt token and one-time jwt refresh token will add protection from token stealing. If someone steals an access token - in works for a short time, if someone steals a refresh token, it would log out the current user because his refresh token is no longer valid. When the user logs in again it invalidates the refresh token of the attacker.

Collapse
learnitmyway profile image
David

"This is achieved by verifying the received token with the exact same key that was used to sign it in the first place" - If I understand this correctly, this would be a symmetric key. It also possible to have an asymmetric key that uses a public and private key. See also stackoverflow.com/questions/329009...

Collapse
gkoniaris profile image
George Koniaris Author

Yes, that's correct. I think it's useful when the client wants to verify that the token was issued by a specific authority. Have you ever used it like this?

Collapse
nklayman profile image
Noah Klayman

Would it be possible/better to split the token between localstorage and a cookie? This would prevent against CSRF and XSS token-stealing attacks at the same time. Part of the token is only accessible by the actual site because it is stored in localstorage, and part of the token in not accessible by JS at all, but could be sent from any site.

Collapse
gkoniaris profile image
George Koniaris Author

Yes, it has been mentioned in other comments too. It’s a nice approach!!

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 Author

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?

Collapse
learnitmyway profile image
David

"when a user closes their browser, the JWT will disappear" - I believe browsers can also restore sessions these days.

Collapse
gkoniaris profile image
George Koniaris Author

Hi David, this is specific to sessionStorage. If you mean traditional sessions through cookies, of course, they are able to restore them, actually, they never lose them. Based on MDN sessionStorage is cleared when you close the tab or the browser.