wasp-lang / wasp

The fastest way to develop full-stack web apps with React & Node.js.
https://wasp-lang.dev
MIT License
13.3k stars 1.18k forks source link

Should we use sessions instead of JWT in Wasp for authentication? What about cookies vs local storage? #573

Open sodic opened 2 years ago

sodic commented 2 years ago

We currently use token-based authentication (JWT and the Authorization header) and persist it inside localStorage. We might want to consider switching:

  1. Change token-based authentication to session-based authentication
  2. Change the persistence mechanism (cookies instead of local storage)

Sessions persisted in cookies seem to be more appropriate for web applications for a multitude of reasons, all exceptionally well covered by this article (as well as its follow-up). I won't repeat the points here, but we can discuss them in the comments.

Martinsos commented 2 years ago

Ha interesting! I thought JWT are relatively well accepted solution and I generally liked them because they don't require us storing anything on the backend and are pretty simple. That said, I certainly haven't investigated other alternatives thoroughly, so thanks for sharing this, we should certainly take this into consideration and see what is the best solution.

sodic commented 2 years ago

they don't require us storing anything on the backend and are pretty simple

I don't think this is actually true in most cases (perhaps the article mentions it). For example, what should the backend do when the user logs out?

The problem becomes even more apparent when you start scaling the backend servers horizontally. Now all instances need to know about invalidated tokens. In other words, we would need to share the state between the servers (for example, using a database) - all in all, no state-related benefits compared to sessions.

Martinsos commented 2 years ago

When logging out with client that uses JWTs, usually that means deleting the JWT from the client side (local storage, cookie, or whatever mechanism is used), and that is it.

You are talking about a specific concern of somebody stealing JWT (at some point when that is possible) and then logging in as user? Sure, that is a possibility, but I don't normally see that connected with the logout in any way. When you use JWTs, normally you won't do any token invalidation at this step - I believe there are other mechanisms to prevent stealing of JWTs, and logout is not considered a mechanism to deal with that.

I guess if done in another way (using sessions and not JWTs), you would expect logout to require next auth attempt to be made exclusively with credentials, therefore eliminating the possibility of smth being stolen during previous communication and being used later?

Btw, if you really wanted to do invalidation of JTWs on logout, which I don't think is common practice (which true, doesn't mean that it is ok), it can be as simple as storing the time of last logout for specific user into their user profile, and then you can reject all JWTs that were created before that. That is a piece of state though, but very simple.

NOTE: I haven't yet read the articles above.

sodic commented 2 years ago

Here's an old comment I forgot to post.

You are talking about a specific concern of somebody stealing JWT (at some point when that is possible) and then logging in as user? Sure, that is a possibility, but I don't normally see that connected with the logout in any way. When you use JWTs, normally you won't do any token invalidation at this step - I believe there are other mechanisms to prevent stealing of JWTs, and logout is not considered a mechanism to deal with that.

I think we misunderstood each other on this. The main point was "there's no way for the backend to know whether a received token has been invalidated (i.e., logged out) without keeping state on the server". After you log out/invalidate a session, the session id is no longer valid authentication for anything (as opposed to a stateless JWT which can still be used). The same applies when revoking roles.

Btw, if you really wanted to do invalidation of JTWs on logout, which I don't think is common practice (which true, doesn't mean that it is ok), it can be as simple as storing the time of last logout for specific user into their user profile, and then you can reject all JWTs that were created before that. That is a piece of state though, but very simple.

Yes, that's correct. It is simple (these are the "negative sessions" I was talking about) but is still more complicated than just storing sessions in the first place (as it's more unusual and poorly supported by frameworks).

Anyway, to sum up. I don't think we can avoid introducing server state in either case because a secure session system must never accept invalidated/stale tokens, and the only way to know about them is by keeping some state on the server. Assuming we agree on this point, JWT offers no benefits compared to Session IDs (and actually comes with several downsides). If we don't agree on this point, I recommend reading the article and I think we will :)

As for the Cookies vs localStorage debate, from what I know, cookies seem to be the obvious choice. I won't spoil why though :)

Anyway, all of the stuff we're talking about here is covered by the article. It's a great read (albeit a bit snarky), check it out when you have the time (the issue isn't anything urgent anyway).

shayneczyzewski commented 2 years ago

I tend to come from the session + cookie camp as well. :D Your articles were pretty compelling @sodic , and I particularly liked the image lol. Here is another one that is often cited: https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens

We could decide as a first step to just put the user id into a cookie (similar to our use of jwts). This will help with security (being cookie based) as well as not require a database. We won't get session invalidation this way though (similar to our current jwt problem), but at least we are trending in the right direction and it makes it easier to do AuthN.

However, we'd need to make it easy to convert to a db-backed session cookies (if we did not go that route from the start).

Some options:

Martinsos commented 2 years ago

Ok guys sold :D!

In the past I never used JWTs completely stateless -> normally I would just store user id in them, nothing more, and then just use them for authentication, not for authorization or smth more. I would fetch data from the user profile (User model) when needed (usually on login). From what I understand from the articles (read them!), cookies are safer than local storage, so that certainly makes sense to me then, to use cookies.

And they are then making the point that if you are using JWTs to distribute only user id, and fetch user data often, you can also just use normal sessions. I get what they mean, but there is some (maybe only at first glance) difference in complexity there. With JWTs that carry user id, I don't really need to persist session data anywhere, at least for the simple implementation. True, if I want to do smth like "log me out on all the machines", I then need to store a bit of data (the datetime of the very moment when that request was made so that all JWTs generated for that user before that moment are rejected), but I can just add that field in user profile (but that is user session :D!!!). So you can kind of get away without thinking about sessions as a thing you need to persist. But without JWT's, I have to start thinking about them immediately, create db model for them (or go for reddis or smth) -> it feels like additional concept is being introduced. When you look at it, it is the same thing mostly, because I can also store session data in user profile as one field and so on, but it feels/sounds more complex compared to JWTs, hm.

Anyway, let's go with cookies and sessions then -> authors of these articles sound like they know what they are saying! We can easily add JWTs on top of this if at some point they will be needed, they don't exclude each other -> but hopefully we won't need them.

That library for Prisma looks interesting @shayneczyzewski ! What about https://github.com/expressjs/session -> I read in one of the articles you shared that this one is popular? Aha, I just read that difference between this and cookie-session is that cookie-session stores the whole session on the client, while session stores just session id on the client in the cookie and session itself is stored on the server/db. I think I prefer the second approach, since it ensures that data is up to date / can't be stale. Ok, you mention that above. I would even go for db solution immediately then, and we can just require you to have db in that case.

shayneczyzewski commented 2 years ago

Sounds good! I think going cookies is a good approach, and like you mentioned we can even decide to just use cookies and store the JWT or user id (the session data) in there and not require any server/db session storage, so it will function similar to what we have now. But with that prisma library it may not be too bad to just assume we store the session id in the cookie and the actual session data in the db from the start as you noted. I'll start looking at this today 👍🏻 Sound good to you @sodic?

Martinsos commented 2 years ago

Awesome! Yeah I don't mind us going for the db immediately!

sodic commented 1 year ago

We tried swapping JWTs+local storage for Sessions+cookies in #635 and failed. Here's the relevant information from Discord

CSRF

Here's OWASP's guide on defending against CSRF: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html. They cover most of the stuff we mentioned, and also go through common antipatterns/mistakes. They also mention a couple of additional techniques (e.g., checking forbidden headers like Host and Referer), but I don't think we need to go down that route yet.

Additional CSRF resources I have bookmarked:

The main problem

Suppose a Wasp app is hosted on two different domains. The default SameSite cookie policy (Lax) won't work, we'd have to set it to None explicitly. Setting SameSite to None requires the Cookie to also be Secure, which in turn forces our application to use HTTPS. That might be an issue for dev setups.

More importantly, If our backend is on a different host than our frontend, the browser will not include the backend's cookies when sending requests (because it's not navigation). Taken from MDN:

Cookies are not sent on normal cross-site subrequests (for example to load images or frames into a third party site), but are sent when a user is navigating to the origin site (i.e., when following a link).

Therefore, to successfully use the API from a different domain, we'd need to explicitly set the SameSite attribute to None.

NOTE: Cookies do not provide isolation by port: https://stackoverflow.com/a/16328399

Deleting the cookie on the client

Solution

Read below for more context, but here's a solution that allows us to have HttpOnly-level of security while also being able to delete the cookies on the client (i.e,. offline logout support). We need two cookies:

  1. An HttpOnly cookie containing the sessionId
  2. A non-HttpOnly cookie containing the sessionId's hash

The request must contain both cookies to be properly authenticated. When performing optimistic logout, we delete the second cookie.

Context

If we want to delete a cookie from the client, it must have its HttpOnly attribute set to false. Setting this attribute to false brings us back to the same level of security we had with storing tokens in localStorage -- the cookie is now accessible from JavaScript. One of the main arguments for using cookies as a storage mechanism (instead of localStorage) was that "Attackers cannot access them from JavaScript," which we now throw down the toilet.

What's interesting is that, since it's impossible to delete an HttpOnly cookie from JavaScript, all websites using cookies must choose between:

  1. Being able to delete the cookies on the client when offline, risking your session IDs being stolen through JS
  2. Using the HttpOnly attribute (which prevents the session ID from being stolen from JS) risking being unable to log out your users when offline

This problem applies to all sites, regardless of whether they are cross-domain. In other words, your app will necessarily have a security flaw, but at least you get to choose which one you want. 😅

Websites using localStorage don't have a choice. They always fall into the first category (i.e., you can log out your users while offline, but your session IDs are always accessible through JS).

Anyway, we need to decide on a compromise. I'm leaning towards using the HttpOnly attribute, mainly because this is the recommended approach (i.e., an often-mentioned security must-have). On the other hand, deleting the session on the client when the server is offline is more of a nice-to-have feature and not something people often think about or expect. Some would probably even say it isn't quite right since it puts the client out of sync with the server.

Cookies' main limitation

All SameSite=None cookies are considered third-party cookies. These cookies are often used for tracking, meaning that almost all browsers block third-party cookies in incognito modes, and some privacy-aware browsers even block them in normal modes. To make matters worse, many users install plugins to block third party cookies or even block them manually.

Basically, if we want to both have cookies and support client and server running on different domains, Wasp sites will not work in incognito mode (and sometimes even in normal modes, e.g., Brave)

infomiho commented 8 months ago

In this PR https://github.com/wasp-lang/wasp/pull/1625 we started using sessions with the Lucia lib, but we didn't change to cookies. Since we didn't use cookies, we didn't consider the issue of CSRF which we should do in the future.

In the future, we talked about having a Wasp deployment setup that will use a single domain e.g. the server will server the client. This will make use of cookies possible for Wasp apps and then we should invest into CSRF protection.

sodic commented 5 months ago

Another important point concerning XSS attacks, pasting @Martinsos's message from Discord for future references

@sodic had interesting thoughs on this XSS thing -> basically that sure, XSS attack can in theory use both cookies and localStorage, but the thing is that today with XSS attacks, when you do one, usually you don't have much space for custom logic and similar, usually you have wiggle room instead for something simple like just stealing the localStorage and sending it somewhere. And when you have that token on your machine you can do whatever you want. So this fact that you can't read HttpOnly cookies seems to mean a big deal in the case of XSS attack, as it makes the efficiency of such XSS attack quite less viable then in case of localStorage. This is at least how I understood it from @sodic , it made sense to me.