Setup session based authentication with NestJS

8/14/2022

One of the critical feature of any backend service is the way used to authenticate users.

While NestJS has an amazing piece of documentation describing how to setup authentication using the passport library and JSON web tokens (JWTs), I found that it lacks some walkthrough on how to get it set up with express sessions.

This guide will walk you through exactly that.

Authentication stack

To build our authentication system, we will use the passport library to help authenticating our users and automatically serializing and deserializing (more on that later) them. We will also make use of the @nestjs/passport library that provides various utilities to make passport integrate well with NestJS.

To keep our users logged for a given amount of time , we will use express sessions through the use of the express-session package, instead of using JSON web tokens as showed in the official documentation.

Application boilerplate

To keep things simple, we are going to mock a lot of the capabilities of our application, including the database.

This section focuses on building a minimal user service that deals with hardcoded user data as well as an authentication service responsible for the validation of users credentials.

If you want to follow along, you don’t need to clone any starter repository. Simply create a new Nest application by issuing the nest new my-nest-app command.

Creating a Users module

Let’s create a new module and service to manage our mocked list of users using the nest CLI:

nest g module users
nest g service users

Let’s head over to our users.service.ts file and hardcode some users.

We also define a method that will help retrieving a user using its username (which we are assuming to be unique), as we’d do with a real database service.

import { Injectable } from '@nestjs/common';

export interface User {
  username: string;
  password: string;
}

@Injectable()
export class UsersService {
  private readonly mockedUsers: User[] = [
    {
      username: 'Alice',
      password: 'pass',
    },
    {
      username: 'Bob',
      password: 'pass',
    },
  ];

  findByUsername(username: string): User | null {
    return this.mockedUsers.find((user) => user.username === username) ?? null;
  }
}

Now, let’s tweak our users.module.ts file to make sure our brand new service is in the provider list and exported from the users module so that we are able to use it from another module:

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

Creating an authentication module

In this section we are creating a new module, service and controller to encapsulate all the authentication capabilities of our application.

Let’s generate our basic boilerplate using the following commands:

nest g module auth
nest g service auth
nest g controller auth

Let’s add a validateUser method to our auth.service.ts file that will be responsible for verifying that the user credentials are valid. We are explicitly making this happen in the authentication service and not in the user service since it’s more tied to authentication than user management.

Note that we are making use of our previously created users service to retrieve the user by its username to then check if the passwords are matching.

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { User, UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  validateUser(username: string, password: string): User {
    const user = this.usersService.findByUsername(username);

    if (!user || user.password !== password) {
      throw new UnauthorizedException('Invalid credentials');
    }

    return user;
  }
}

That way of storing users and comparing passwords is obviously really insecure and should never be used in production. In a traditional and secure configuration, you would hash the password in a database and then compare the stored hash with the plain text password sent by the user that tries to log in.

Passport integration

Now that we are done with our mocked user database and our basic authentication logic, it’s time to integrate passport to our application.

Passport is a library that is responsible for handling the authentication process using what are known as strategies. This library also injects a lot of utility functions and useful data into the request object.

We will need to install the following dependencies:

npm install @nestjs/passport passport passport-local

We are installing the passport package itself, the passport-local strategy that is used for local authentication using passport, as well as the NestJS integration of passport that will make integrating it with our app way easier.

We will also need to install some development dependencies to benefit from full type safety:

npm install --save-dev @types/passport @types/passport-local

Creating our local strategy

Let’s implement our actual passport strategy by creating a new Injectable class that inherits from the special PassportStrategy utility exported from @nestjs/passport.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }
}

The PassportStrategy is what is know as a mixin : a function that returns a class that our class is extending (inheriting from, in more traditional OOP terms).

It takes the specific strategy we are implementing as an argument, here the local strategy.

Make sure you are importing Strategy from the passport-local package and not the passport one. That’s a common source of errors.

In the constructor of the class, don’t forget to call the super method that calls the constructor of the extended class. This super function can be passed an object that contains configuration properties for the specific strategy we are dealing with.

For example, we could instruct passport to expect a field named email in our request body instead of the default of username by calling super like that:

super({
    usernameField: 'email'
})

Although we don’t need that for our example, changing the name of the username field is very common.

As you may have noticed, we injected our AuthService inside the class’s constructor as we would for any other provider. This works because our strategy is a provider that is registered in our AuthModule's provider list.

Now let’s implement the validate method of our strategy. This method is going to be automatically called by passport when user authentication is requested for that strategy.

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  validate(username: string, password: string): User {
    return this.authService.validateUser(username, password);
  }
}

As we already defined the authentication logic in our AuthService, the implementation of the validate method is straightforward. We return the user returned by the validateUser method. Passport expects this user, so that it can inject it in the request object as the request.user property, making it available to whichever method is called after.

Configuring the AuthModule

Providing the strategy

For everything to work, make sure the LocalStrategy injectable we just made is included in the providers array of the AuthModule like so:

import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module.ts';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './local.strategy';

@Module({
    imports: [UsersModule],
  providers: [AuthService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

Make sure you also imported the UsersModule in the imports array for the dependency injection of UsersService to work.

Importing the Passport module

For the whole passport set up to work, we need to import the PassportModule exported by the @nestjs/passport package.

Traditionally, it’s simply imported like that:

import { PassportModule } from '@nestjs/passport';

@Module({
    imports: [PassportModule]
})

While this should work for now as we don’t have sessions set up, this is not enough.

In our case, we want to instruct the passport module that we will indeed be dealing with actual sessions, so that it can tweak some stuff internally for that matter.

We can achieve that by registering the PassportModule dynamically, by calling the static register method on it. Check this out:

@Module({
    imports: [
        PassportModule.register({
            session: true
        })
    ]
})

Authentication with passport using NestJS guards

Let’s focus on creating a login route that will make use of the passport local strategy we just created.

First, let’s head over our auth.controller.ts file and add a few routes:

import { Controller, Get, Post } from '@nestjs/common';

@Controller('auth')
export class AuthController {
  @Post('login')
  login() {
    /* we are inside this method when user logged in successfully using username and password */
  }

  @Post('logout')
  logout() {
    /* destroys user session */
  }

  @Get('protected')
  protected() {
    return {
      message: 'This route is protected against unauthenticated users!',
    };
  }
}

We will deal with the logout and protected routes when setting up sessions, but for now let’s focus on invoking passport when POSTing on /auth/login to authenticate the user.

Using the AuthGuard

To request passport authentication using a strategy on any route, we are using the AuthGuard exported by the @nestjs/passport package, which is a guard mixin that takes the name of the desired strategy as an argument.

We can simply decorate the login controller method like so:

@UseGuards(AuthGuard('local'))
@Post('login')
login(@Req() request: any) {
  return request.user; /* the user we are returning in our local strategy's validate method
}

From inside the login method, we are now referring the user property of the internal request object, that has been populated by passport in case the authentication was successful.

Now that the guard is in place, we can try to make a request to authenticate, using curl in this example:

curl -X POST -H 'Content-Type: application/json' -d '{ "username": "Alice", "pass": "pass" }' http://localhost:3000/auth/login

Because the credentials are valid, we should get the user profile as a return.

If you attempt to change anything in that payload making it invalid, you’d be able to witness the fact that the guard prevents access to the route and returns an UnauthorizedException , that is indeed leveraged by our authService.validateUser method itself called in our local strategy.

If that makes sense, we are done with authenticating our user using a passport strategy. Congratulations!

Storing the authentication state using sessions

Now that we authenticate our users using their username and password, we want to actually store the information that they are logged in (for a given period of time) somewhere.

The official NestJS documentation makes use of JSON web tokens for that matter. JWTs are stored on the client side (in a cookie or in the browser’s local storage) and are stateless, which means that they can’t be revoked.

As opposed to JWTs, sessions are essentially stored in a session store on the server side. Because of their nature, sessions can be revoked by simply removing them from the session store, allowing more flexibility.

We will focus on implementing persistent authentication through the use of sessions in that section.

In this walkthrough, we are using the default express-session store, which is the application's memory. This is obviously not secure and not intended for use in production. You should check out the list of supported stores.

Using a session middleware

To take care of creating and storing sessions, we want to use a session middleware that will execute the necessary logic for each route of our application.

The common session middleware of choice is express-session if the Nest application uses express as the underlying platform, which we are assuming is the case.

To start, let’s install the following dependencies:

npm install express-session
npm install --save-dev @types/express-session

Let’s open our auth.module.ts and register the express-session middleware first, then the passport.session next.

/* ... */
import { SessionModule } from 'nestjs-session';

@Module({
  imports: [
    UsersModule,
    PassportModule.register({
            session: true
        })
  ],
  providers: [AuthService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
          .apply(
            expressSession({
              secret: 'SOME SESSION SECRET',
              resave: false,
              saveUninitialized: false,
            }),
            passport.session(),
          )
          .forRoutes('*');
      }
}

By doing that, we instructed express-session to generate a unique session for each one of our visitors.

The passport.session middleware is responsible for injecting some utility functions in the request object and checking whether or not the session belongs to an authenticated user.

We can verify that the session is indeed created by adding the following code to any route handler:

import { Session } from '@nestjs/common';
import { Session as ExpressSession } from 'express-session';

@Controller('somewhere')
export class SomeController {
    @Get()
    someMethod(@Session() session: ExpressSession) {
        return session;
    }
}

If everything was set up correctly, we should get the following JSON when we hit the route:

{
  "cookie": {
    "originalMaxAge": null,
    "expires": null,
    "httpOnly": true,
    "path": "/"
  }
}

Neat! That said, we don’t have anything related to the user in here. That’s perfectly normal since we didn’t tell passport to use the session provider to attach user data to the session. Currently, we don’t have any idea about whether or not this session is authenticated or not.

Create a user session

User serialization and deserialization

First of all, we need to create a PassportSerializer responsible for user serialization and deserialization. This is required when using sessions because passport needs to know how to retrieve the user from a serialized value (that will be stored inside the session), and vice-versa.

Let’s create a new passport serializer at src/auth/user.serializer.ts:

import { Injectable } from '@nestjs/common';
import { PassportSerializer } from '@nestjs/passport';
import { User, UsersService } from '../users/users.service';

@Injectable()
export class UserSerializer extends PassportSerializer {
  constructor(private readonly usersService: UsersService) {
    super();
  }

  serializeUser(user: User, done: Function) {
    done(null, user.username);
  }

  deserializeUser(username: string, done: Function) {
    const user = this.usersService.findByUsername(username);

    if (!user) {
      return done(
        `Could not deserialize user: user with ${username} could not be found`,
        null,
      );
    }

    done(null, user);
  }
}

Serializing the user basically strips most data from the user object, only leaving minimal information that can be used to retrieve the complete object when needed, which is known as deserialization.

Don’t forget to add the serializer to our AuthModule provider list:

providers: [AuthService, LocalStrategy, UserSerializer]

Attach user data to session

The passport.session middleware automatically injects a set of handful methods in the request object that allow us to log in or log out the user, or even check if the user is authenticated or not.

By default, the guard returned by AuthGuard('local') doesn’t call the request.login method that is responsible for creating a persistent user session.

One way to implement it correctly is to create our very own guard by extending the guard we were using until now.

Let’s create a brand new guard then:

nest g guard auth/guards/local-auth

And here is a possible implementation:

import {
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
  constructor() {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    await super.canActivate(context);

    const request = context.switchToHttp().getRequest() as Request;

    await super.logIn(request);

    return true;
  }
}

First, we call the guard logic of the extended AuthGuard('local') guard which calls our local strategy. As we implemented it, we know that the call to super.canActivate would throw an UnauthorizedException if the credentials provided in the request body aren’t valid.

The call to the extended guard’s logIn method allows us to create a persistent log in session by serializing the user using our PassportSerializer and storing that value in the session for its whole lifetime.

Don’t forget to replace the guard by the new one we just created in the controller:

@UseGuards(LocalAuthGuard)
@Post('login')
login(@Req() request: any, @Session() session: ExpressSession) {
    console.log({ session });

    return session;
 }

Let’s hit the route with valid credentials and check the content of the session object now:

{
  "cookie": {
    "originalMaxAge": null,
    "expires": null,
    "httpOnly": true,
    "path": "/"
  },
  "passport": {
    "user": "Alice"
  }
}

This works exactly as intended. We do have a passport field that itself contains a user field that holds the serialized user.

Check if session is authenticated

Now that we are able to log in users using plain credentials, the final step is to create a guard which sole responsibility is to check whether or not the session belongs to an authenticated user.

Using the set of utilities injected by the passport.session middleware inside the request object, this is easily achieved.

Let’s create another guard:

nest g guard auth/guards/is-authenticated

Inside which we simply check if the session is authenticated using the request.isAuthenticated method:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';

@Injectable()
export class IsAuthenticatedGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest() as Request;

    return request.isAuthenticated();
  }
}

We are now able to decorate anything with that guard in order to check if the session is authenticated or not, let’s do so for our previously created /auth/protected route:

@UseGuards(IsAuthenticatedGuard)
@Get('protected')
protected() {
  return {
    message: 'This route is protected against unauthenticated users!',
  };
}

When a request to this route is made, the passport.session middleware looks at the session object and checks if there is a serialized user in it. If this is the case, it deserializes it to the actual user using our previously created PassportSerializer , and injects it in the request object as the request.user property. If this is not the case, the guard will prevent access to the route.

Implementing user logout

One of the benefits of using sessions over JWTs is that we can revoke a session at anytime.

To logout a user, passport.session injects a logout method on the request object that takes care of everything needed.

Therefore, we can implement our logout route in a very straightforward manner:

import { Req } from '@nestjs/common';
import { Request } from 'express';x

@UseGuards(IsAuthenticatedGuard)
@Post('logout')
async logout(@Req() request: Request) {
  const logoutError = await new Promise((resolve) =>
    request.logOut({ keepSessionInfo: false }, (error) =>
      resolve(error),
    ),
  );

  if (logoutError) {
    console.error(logoutError);

    throw new InternalServerErrorException('Could not log out user');
  }

  return {
    logout: true,
  };
}

Final words

In this post we learned how to set up authentication in a NestJS application using passport and how to store the authentication state inside sessions using express-session.

Because sessions are stored on the server side of the application, we are given much more flexibility when it comes to session management and storage.