Stop Using JSON Web Tokens For Authentication. Use Stateful Sessions Instead | by Francisco Sainz | Apr, 2022

Photo by iMattSmart on Unsplash

I’m tired of seeing the same tutorials pop up every couple of weeks.

  • “JWTokens are the recommended auth method because of scalability.”
  • “JWTokens are easier to use.”
  • “JWTokens are stateless, so you don’t use memory on the server.”

Let me tell you something. These people probably don’t know any better.

I am sure their intentions are good, but they share an un-secure way of authenticating and authorizing users, at least for web applications.

I’ve been recently building a production-ready web app, researching and implementing things the right way.

I’ve also interned as a Software Engineer at Twilio and Amazon, so I know what I am talking about.

However, please don’t feel bad; I used JWT when starting I was starting out because I didn’t know any better.

Let’s get started!

Photo by Benjamin Davies on Unsplash

With that out of the way, let’s go through the flow to authenticate users with JWT.

  • The user enters their username and password — When the user clicks the sign-in button, a request is sent to the server to verify the user’s credential with the database.
  • The server successfully authenticates the user — The server now creates and signs a JWT using a secret password and returns it in the response.

Tutorials usually set the expiration to about one week to 30 days.

  • The Client Receives the JWT in the response — The developer (in a client like chrome) receives it, applies some logic, and then stores it, usually in Local Storage.
  • The Client uses info stored in the Token to render conditionally — Usually, tutorials use fields like a user’s email, username, and boolean fields like isAdmin.
  • The Client Adds the Token as a header for every request — If the Token exists in Local Storage, the user’s session is active.

The server can now check the user’s identity by decrypting the signed Token on every request.

The token will be valid until it expires.

Photo by Randy Laybourne on Unsplash

If your developer senses aren’t tingling yet, don’t worry, I highlighted some of the red flags of the approach, and we will break them down even further below.

The response contains the JWT.

The first red flag we encounter is returning the Token in the response. Because of this, the front-end code has free access to read and store the Token.

Access to the Token enables cross-site scripting attacks to steal the user’s identity and send requests on their behalf. Since we don’t have session information about the users, there’s probably no way to know.

JWT’s are always valid until they expire

Since JWT auth is stateless, there is no way to revoke the user’s session once the server signs a valid token.

Accordingly, using long expiration windows + unsafe storage is the perfect combination for a hacker to inflict severe damage to our users.

The only way to revoke them is to change the signing secret, and this would essentially log out your whole user base since all tokens would be rendered invalid.

Local Storage is not safe.

Local Storage is not safe since anyone can access it in the browser.

Cross-Site Scripting can retrieve tokens from the local Storage as it isn’t encrypted or protected.

Information may be out of sync.

This issue is not as grave as the others, but I wanted to mention it.

Storing fields like isAdminthe user’s address, etc., might not be inherently unsafe, but here’s the issue.

Since tokens are stateless, there is no way to update them when info changes. A user’s username, email, or permissions could be out of sync with the actual values ​​in the database.

Cross-Origin Requests are allowed.

With JWT, anyone with the Token can send valid requests.

Malicious sites can send requests to your website from unsafe or fake domains that may look like your website, and the browser will allow it.

You can minimize this risk by using CORS, but the tutorial probably didn’t mention that.

Photo by Nick Fewings on Unsplash

These tokens aren’t safe for authenticating web apps, but that doesn’t mean they are useless. On the contrary, several use cases work great for these tokens.

Use JWT when:

  • The expiration window is tiny. (ex. 5 min)
  • The request does not involve storing the token in a browser
  • The request does not require encryption.

A real-life example use case

The perfect example is controlling access to resources like file downloads.

Let’s say that your user has bought a virtual product a month ago and wants to download it again.

Are you going to have an open link that anyone can use to download your virtual product?

JWT tokens come in handy since you can create short-lived access tokens that verify the user’s identity and temporarily grant access to the purchased content.

The token is not stored anywhere, and it expires very fast. Therefore, it allows you to process verifiable transactions with ease.

If you’ve ever integrated 3rd party sign-on with Google or other providers, that’s an excellent example of when to use JWT.

Photo by Frank on Unsplash

But wait, if I can’t use JWT tokens, how can I authenticate my users?

This article would be useless if it didn’t include a better way.

A stateful session means that the server stores the user’s sessions in memory or the database.

Although this does come with some tradeoffs, it eliminates all the issues and security concerns of the previous approach when appropriately implemented.

You can keep track of a user’s session activity.

With stateful sessions, you can store helpful info like the user’s IP address, session duration, last request timestamp, and view how many active sessions each user has.

The server can revoke sessions on demand.

Upon triggering a warning, (imagine the user has three active sessions from different countries), sessions can be revoked on demand to prevent stolen tokens from being used—no need to wait for expiration every 30 minutes.

HTTP-Only Cookies are safe.

You must store stateful session tokens (like a UUID string) in HTTP-Only Cookies.

An HTTP-Only cookie means that the Cookie is automatically attached to each Client’s request, and no one can access the Token in the browser, not even you!

HTTP-Only Cookies prevent cross-site requests by default

HTTP Only Cookies should have strict settings regarding cross-site requests. Cookies won’t work if the request is sent from a 3rd party domain by default.

We’ll dive deeper into this later.

Sessions aren’t cryptographically expensive.

You don’t have to validate signed JWT sessions since the Token can be mapped to the user id and stored in a memory-based database like Redis for lighting-fast access and read operations.

Photo by Bruno Kelzer on Unsplash

Usually, stateless is a preferred approach since it allows services to run in great numbers without any dependencies (state)

Using stateful sessions does introduce a potential new challenge for us.

Scalability

JWT tutorials love talking about how their Todo List React App with 12 users needs to be scalable to serve millions of active users.

While this is a valid issue and a fantastic future problem, it’s not good to put scalability above security.

Only address the immediate needs of your application, and scalability is probably not one of them. A single server can serve hundreds, if not thousands of users.

Photo by Marc-Olivier Jodoin on Unsplash

If scalability is an active concern for your application, you don’t have to worry; We will fix it soon.

Making our Stateful Sessions Stateless

An application is stateless if it does not need to store any state in the same instance it is running on.

If your database ran inside the same server instance as your application, it wouldn’t be stateless.

However, if you run your database independently from your server instance, your server is stateless since its only purpose is to process business logic.

Storing data is the database’s concern.

Storing our Sessions in a Memory-Based Database

Your server should require session data for every authenticated request to your server. For this reason, we want to optimize Read and Write operations.

Using our regular SQL or NoSQL database would be very taxing and could potentially induce high costs and slowdowns.

Using Redis

Redis is an in-memory (key, value) pair database which allows for fast Read and Write access. However, I recommend you do your research as there are various alternatives.

For example, AWS provides Redis clusters which ensure that your operation remains scalable automatically with autoscaling groups.

Photo by Solen Feyissa on Unsplash

Finally, let’s go through the workflow of using Stateful Sessions, as we did not so long ago with JWT.

The user enters their username and password.

When the user clicks the sign-in button, a request is sent to the server to verify the user’s credential using the database.

The server successfully authenticates the user.

The server creates a token (UUID), maps it to the user’s database ID, and stores it in Redis.

The attach serveres the Cookie to the HTTP response sent to the browser.

The Client Checks if the Token is valid.

Simply having an HTTP-Only cookie in the header does not mean a session is active.

The browser sends a GET request to the server endpoint that handles user session data. Since the Cookie provides the server with this info, this request requires no parameters.

The front-end can now store the session data in the application without keeping the actual Token.

The Client uses info stored in the Token to render conditionally

Usually, tutorials use fields like a user’s email, username, etc. This time, the information is usually up to date since it is updated every time the Client updates the website.

The HTTP-Only Cookie is sent automatically on every request

The Token will be valid until it expires. However, the server can revoke it on demand.

Photo by Lindsay Henwood on Unsplash

This article went over JWT and its shortcomings as a web auth solution. Then we found a better (secure) way of implementing auth for our web apps.

However, I showed a straightforward implementation using stateful sessions. You might be wondering, what about expiration? Even one week can be a long expiration window.

Refresh Tokens

I did not talk about refresh tokens to keep things simple and this article from becoming any longer, but here is a general idea.

You have two tokens: the authentication token, which verifies your identity, and the refresh token.

Auth tokens can then be short-lived, for example, 1–2 days. The browser will constantly be checking while the user is actively using their sessions to see if the auth token expires soon. When it detects this, it uses the refresh token, with has a longer-lived expiration, to request a new auth token before the previous one expires.

Long-Lived Refresh tokens introduce new concerns and complexity like token rotation and token reuse checks, but I’ll leave that topic for another post.

Leave a Comment