API with NestJS #35. Using server-side sessions instead of JSON Web Tokens

JavaScript NestJS

This entry is part 35 of 37 in the API with NestJS

So far, in this series, we’ve used JSON Web Tokens (JWT) to implement authentication. While this is a fitting choice for many applications, this is not the only choice out there. In this article, we look into server-side sessions and implement them with NestJS.

You can find the code from this article in this repository

The idea behind server-side sessions

At its core, HTTP is stateless, and so are the HTTP requests. Even though that’s the case, we need to implement a mechanism to recognize if a person performing the request is authenticated. So far, we’ve been using JSON Web Tokens for that. We send them to the users when they log in and expect them to send them back when making subsequent requests to our API. This encrypted token contains the user’s id, and thanks to that, we can assume that the request is valid.

If you want to know more about JSON Web Tokens, check outAPI with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

With the above solution, our application is still stateless. We can’t change the JWT token or make it invalid in a straightforward way. The server-side sessions work differently.

We create a session for the users with server-side sessions when they log in and keep this information in the memory. We send the session’s id to the user and expect them to send it back when making further requests. When that happens, we can compare the received id of the session with the data stored in memory.

The advantages and disadvantages

The above change in approach has a set of consequences. Since we are storing the information about the session server-side, it might become tricky to scale. The more users we have logged in, the more significant strain it puts on our server’s memory. Also, if we have multiple instances of our web server, they don’t share memory. When due to load balancing, the user authenticates through the first instance and then accesses resources through the second instance, the server won’t recognize the user. In this article, we solve this issue with Redis.

Keeping the session in memory has its advantages, too. Since we have easy access to the session data, we can quickly invalidate it. If we know that an attacker stole a particular cookie and can impersonate a user, we can easily remove one session from our memory. Also, if we don’t want the user to log in through multiple devices simultaneously, we can easily prevent that. If a user changes a password, we can also remove the old session from memory. All of the above use-cases are not easily achievable with JWT. We could create a blacklist of tokens to make tokens invalid, but unfortunately, it wouldn’t be straightforward.

Defining the user data

The first thing we do to implement authentication is to register our users. To do that, we need to define an entity for our users.

In this article, we use TypeORM. Another suitable alternative is Prisma. If you want to know more, check out API with NestJS #32. Introduction to Prisma with PostgreSQL

user.entity.ts

Above, we use to make sure that we don’t respond with the user’s password. If you want to dive deeper into this, check out API with NestJS #5. Serializing the response with interceptors

We also need to be able to perform a few operations on the collection of users. To that, we create the .

users.service.ts

createUser.dto.ts

Managing passwords

The crucial thing about the registration process is that we shouldn’t save the passwords in plain text. If a database breach happened, this would expose the passwords of our users.

To deal with the above issue, we hash the passwords. During this process, the hashing algorithm converts one string into another string. Changing just one character in the passwords completely changes the outcome of hashing.

The above process works only one way and, therefore, can’t be reversed straightforwardly. Thanks to that, we don’t know the exact passwords of our users. When they attempt to log in, we need to perform the hashing operation one more time. By comparing the hash of the provided credentials with the one stored in the database, we can determine if the user provided a valid password.

Using the bcrypt algorithm

One of the most popular hashing algorithms is bcrypt, implemented by the bcrypt npm package. It hashes the strings and compares the plain strings with hashes to validate the credentials.

The bcrypt library is rather straightforward to use. We only need the and functions.

The authentication service

We now have everything we need to implement the feature of registering users and validating their credentials. Let’s put this logic into the .

authentication.service.ts

There are quite a lot of things happening above. We break it down part by part in API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

Using server-side sessions with NestJS

To implement authentication with server-side sessions, we need a few libraries. The first of them is express-session.

In this article, we also use the passport package. It provides an abstraction over the authentication and does quite a bit of the heavy lifting for us.

Different applications need various approaches to authentication. Passport refers to those mechanisms as strategies. The one we need is called passport-local. It allows us to authenticate with a username and a password.

When our users authenticate, we respond with a session ID cookie, and for security reasons, we need to encrypt it. To do that, we need a secret key. It is a string used to encrypt and decrypt the session ID.

Changing the secret invalidates all existing sessions.

We should never hardcode the secret key into our codebase. A fitting solution is to add it to environment variables.

app.module.ts

.env

When we have all of the above set up, we can apply the appropriate middleware to turn on both express-session and passport.

main.ts

A significant thing to understand about the above code is that by default, the express-session library stores the session in the memory of our web server. This approach might not scale very well and will not work properly if we have multiple app instances. We will deal with this issue later in this article with Redis.

The official NestJS documentation sets the and flags to false. We can find a very good explanation of this on stackoverflow.

Using passport to log in and authenticate

Since we aim to authenticate our users with a username and a password, we need to use the passport-local strategy. To configure it, we need to extend the class.

local.strategy.ts

Passport calls the function for every strategy. For the local strategy, Passports requires a username and a password. Our application uses email as the username.

The authentication flow begins when the controller intercepts the request sent by the user.

authentication.controller.ts

Quite a few important things are happening there. First, let’s investigate the .

logInWithCredentialsGuard.ts

Above, we aim to verify the credentials provided by the user. This is something the does out of the box when the method is called by the user accessing the route.

We also need to call the method to initialize the server-side session. When we look under the hood of NestJS, we can see that this method calls . It is a function added to the request object by Passport. It creates the session and saves it in memory. Thanks to that, the Passport middleware can attach the session id cookie to the response.

We need to specify the exact data we want to keep inside the session. To manage it, we need to create a serializer.

local.serializer.ts

The function determines the data stored inside of the session. In our case, we only store the id of the user.

The result of the function gets attached to the request object. By calling the function, we get the complete data of the logged-in user, and we can access it through in the controller.

Authenticating with the session id cookie

In the above screenshot, we can see that we respond with the cookie when the user logs in, set through the header. We now expect the user to attach this cookie when performing further requests to our API.

An important thing about the above cookie is the flag set to true. Because of that, the browser can’t access it directly through JavaScript. It makes the cookie more secure and resistant to attacks like cross-site scripting.

If you want to know more about cookies, read Cookies: explaining document.cookie and the Set-Cookie header

We now need to create a NestJS guard that verifies the session id cookie. To define a guard, we need to implement the interface.

cookieAuthentication.guard.ts

The function is attached to object by Passport. Therefore, we don’t need to implement it ourselves. The returns only if the user is successfully authenticated.

We now can attach the to a route. By doing so, we specify that a valid session is required to access it.

Logging the user out

When we’ve implemented authentication with JWT, our way of logging the user out wasn’t perfect. Back then, we’ve just sent a header that aimed to remove the token from the browser. Unfortunately, this didn’t make the token invalid.

With server-side sessions, logging out is a lot better.

authentication.controller.ts

The function is attached to the object by Passport. Calling it removes the session from the memory of the webserver. Even if someone retrieved the cookie and tried to reuse it, the session is long gone and can’t be accessed. The above provides an additional layer of security compared to JWT.

As an additional step, we can also remove the cookie from the browser of our user. The easiest way to do that is to set to 0.

Improving our sessions with Redis

By default, the express-session library keeps all of the sessions in the memory of the webserver. The more users we’ve got logged in, the more memory our server uses. Restarting the webserver causes all of the sessions to disappear. It might also create issues if we’ve got multiple instances of our app. When the user authenticates the first instance and then accesses the API through the second instance, the server can’t find the session data. We can solve this issue by using Redis instead of storing the sessions directly in the server’s memory.

So far, in this series, we’ve used Docker Compose to set up the architecture for us. This is also a fitting place to set up Redis. By default, it works on port .

docker-compose.yml

To connect to our Redis instance, we need to add a few environment variables.

app.module.ts

.env

To make the express-session library work with Redies, we need to add a few dependencies.

The last step is using all of the above in our function.

main.ts

Summary

In this article, we’ve gone through the advantages and disadvantages of server-side sessions. We’ve implemented a complete authentication flow using Passport and the express-session library. We’ve also improved it using Redis instead of keeping the sessions directly in the server’s memory. By doing all of the above, we’ve achieved a suitable alternative to JSON Web Tokens.

Series Navigation<< API with NestJS #34. Handling CPU-intensive tasks with queuesAPI with NestJS #36. Introduction to Stripe with React >>
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Chase Gibbons
Chase Gibbons
16 days ago

this is a wonderfully robust and production-ready tutorial, thnx for sharing all the knowledge!!