DEV Community

Cover image for User Authentication with Passport JS and JWT in Nest JS
Andisi (Roseland) Ambuku
Andisi (Roseland) Ambuku

Posted on • Edited on

User Authentication with Passport JS and JWT in Nest JS

Nest JS is the lovechild of Typescript and Node JS (hurrah type-safety for the backend). The two come together to create a modular and robust framework that is easy to scale and maintain.

This framework is ideal for developers who prefer convention over configuration as plenty of features come with the framework, for example, Decorators that define metadata for classes and Middleware that intercepts requests and responses for efficient data transmission.

Prerequisites

Ensure you have Node JS(version >= 16) installed on your machine and then proceed to the next section where we set up a REST API.

Setup

For this tutorial, we shall set up a REST API using the following steps:

Open your operating system's terminal and key in the following commands.

npm i -g @nestjs/cli
nest new blog-api

The first command installs the nest CLI globally in your system. Note: installation takes a long time so don't be alarmed.

Once done, the second command creates a project(feel free to call the project whatever name you wish). Ensure you choose REST from the options to create a REST API. The blog-api directory will have some boilerplate files, node modules, and a src folder with core files.

Setting up TypeORM

TypeORM is an object-relational mapper that supports the latest JavaScript features from small to large-scale applications.

The first step to configuring TypeORM is to install the relevant packages

npm i pg typeorm @nestjs/typeorm @nestjs/config

The packages installed are as follows:

typeorm - the package for TypeORM.

pg - a postgres driver to connect typeorm and postgres database.

@nestjs/config - Nest JS module for configuration.

@nestjs/typeorm - Nest JS module for TypeORM.

Configuring a datasource file

After installing the relevant packages, we need to configure a datasource file to enable the use of features like migrations.

A datasource is a file where the connection settings for our database. It also sets the first database connection.

A migration is a file with SQL queries that is used to modify an existing database and update its schema.

Create a database using pgAdmin or psql CLI. If you are using the psql CLI then run the following command and add the database's name to the typeorm configs at the .env

createdb databasename

Create a file at the root of the Nest application and call it dataSource.ts.

It is a good security practice to have sensitive data like database configurations stored in a .env file. For the port, host, username, password, and database add the configurations in a .env file and import the environment variables to this file.

import { DataSource } from 'typeorm';
import { ConfigService } from '@nestjs/config'; //installed package
import { config } from 'dotenv';

config();

const configService = new ConfigService();

export default new DataSource({
  type: 'postgres',
  host: configService.get('TYPEORM_HOST'),
  port: configService.get('TYPEORM_PORT'),
  username: configService.get('TYPEORM_USERNAME'),
  password: configService.get('TYPEORM_PASSWORD'),
  database: configService.get('TYPEORM_DATABASE'),
  entities: ['src/**/*.entity.ts'],
  migrations: ['migrations/**/*{.ts,.js}'],
});
Enter fullscreen mode Exit fullscreen mode

Let us discuss the configurations in this file:

const configService = new ConfigService();: This line creates an instance of a ConfigService class, which is likely a custom class used to manage configuration settings for the application. It seems to provide a way to retrieve configuration values.

export default new DataSource({ ... });: This line exports a new instance of the DataSource class, which is the main configuration object used by TypeORM to connect to a database.

Configuration Object Properties:

type: 'postgres': Specifies that PostgreSQL is the database type being used.

host: configService.get('TYPEORM_HOST'): Retrieves the database host from the configuration service using the key 'TYPEORM_HOST'.

port: configService.get('TYPEORM_PORT'): Retrieves the database port from the configuration service using the key 'TYPEORM_PORT'.

username: configService.get('TYPEORM_USERNAME'): Retrieves the database username from the configuration service using the key 'TYPEORM_USERNAME'.

password: configService.get('TYPEORM_PASSWORD'): Retrieves the database password from the configuration service using the key 'TYPEORM_PASSWORD'.

database: configService.get('TYPEORM_DATABASE'): Retrieves the database name from the configuration service using the key 'TYPEORM_DATABASE'.

entities: ['src/*/.entity.ts']: Specifies the paths to entity files. These files define the structure of your database tables and are used by TypeORM to interact with the database.

migrations: ['migrations/*/{.ts,.js}']: Specifies the paths to migration files. Migrations are scripts that manage changes to the database schema over time.

Create a migrations folder at the root of the Nest JS project for our migration files.

Add the following to the existing scripts in package.json to enable typeorm migrations to work.

"scripts": {
    "typeorm": "ts-node ./node_modules/typeorm/cli",
    "typeorm:run-migrations": "npm run typeorm migration:run -- -d ./dataSource.ts",
    "typeorm:generate-migration": "npm run typeorm -- -d ./dataSource.ts migration:generate",
    "typeorm:create-migration": "npm run typeorm -- migration:create",
    "typeorm:revert-migration": "npm run typeorm -- -d ./dataSource.ts migration:revert"
  },
Enter fullscreen mode Exit fullscreen mode

App module

Add the typeorm module along with the configurations to the app module at imports array.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
import { AuthModule } from './auth/auth.module';


// eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config();

const {
  TYPEORM_HOST,
  TYPEORM_USERNAME,
  TYPEORM_PASSWORD,
  TYPEORM_DATABASE,
  TYPEORM_PORT,
} = process.env;

@Module({
  imports: [TypeOrmModule.forRoot({
      type: 'postgres',
      host: TYPEORM_HOST,
      port: parseInt(TYPEORM_PORT),
      username: TYPEORM_USERNAME,
      password: TYPEORM_PASSWORD,
      database: TYPEORM_DATABASE,
      entities: [join(__dirname, '**', '*.entity.{ts,js}')],
    }),
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Adding the user authentication feature

User authentication is an important feature in any application. It allows users to create accounts in our application and access features provided therein. It is also a security measure to protect user data.

To create this feature we shall make use of the resource generator in Nest.

nest g resource auth

It generates the entity, controller, module, and service files we require for this feature.

User Entity

The first thing we do is to model the user. We shall create the database columns or the user entity.

//user.entity.ts
import {BaseEntity, PrimaryGeneratedColumn, Entity, Column, Unique,
OneToMany,} from 'typeorm';
import * as bcrypt from 'bcrypt';

@Entity()
@Unique(['email'])
export class User extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  username: string;

  @Column()
  email: string;

  @Column()
  password: string;

  @Column()
  salt: string;

  async validatePassword(password: string): Promise<boolean> {
    const hash = await bcrypt.hash(password, this.salt);
    return hash === this.password;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let us discuss the code in this file.

@Entity(): This decorator indicates that the class User is an entity, representing a database table. In this case, it represents the "User" table.

@unique(['email']): This decorator specifies that the "email" column in the "User" table should have unique values. This helps enforce uniqueness for email addresses in the database.

export class User extends BaseEntity { ... }: This defines the User class, which extends BaseEntity. BaseEntity is a class provided by TypeORM that contains common properties and methods for entities.

@PrimaryGeneratedColumn('uuid'): This decorator indicates that the id property is a primary key column that will be generated using UUIDs (Universally Unique Identifiers).

@Column(): These decorators indicate that the email, password, and salt properties are columns in the table. The email and password columns are required, while the salt column is used for password hashing.

We shall hash user passwords with bcrypt before storing them in the database to secure user passwords.

The salt is a randomly generated value that is attached to an encrypted password to uniquely identify it in the event two or more users (unknowingly) register with the same passwords.

User Data Transfer Object

A data transfer object (DTO) is a design pattern used to define a data structure that carries data between different layers of an application, often between the client and the server. DTOs are used to encapsulate and transfer data in a structured and controlled way.

We shall have the following DTOs for this feature: the login DTO and the signup DTO.

Signup DTO

The DTO is used to validate the inputs that we get from the user when they signup. We use the class validator library to validate the database columns.

//signup.dto.ts
import { IsString, IsEmail, MinLength, Matches } from 'class-validator';

export class SignupDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  //regex for password to contain atleast one uppercase, lowercase, number and special character
  @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, {
    message:
      'password must contain uppercase, lowercase, number and special character',
  })
  password: string;

  @IsString()
  username: string;
}
Enter fullscreen mode Exit fullscreen mode

export class SignupDto { ... }: This defines the SignupDto class.

@IsEmail(): This decorator specifies that the email property should be a valid email address.

@IsString(): This decorator specifies that the password and username properties should be of string type.

@MinLength(8): This decorator specifies that the password property should have a minimum length of 8 characters.

@Matches(/((?=.\d)|(?=.\W+))(?![.\n])(?=.[A-Z])(?=.[a-z]).*$/, { ... }): This decorator specifies that the password property should match a specific regex pattern that enforces certain complexity rules:

(?=.*\d): At least one digit (number) should be present.

(?=.*\W+): At least one special character should be present.

(?=.*[A-Z]): At least one uppercase letter should be present.

(?=.*[a-z]): At least one lowercase letter should be present.

.$: Match any characters (the . before $ ensures that the rule applies to the entire string).

The provided message is used to specify a custom error message if the password does not meet the complexity requirements.

password: string;: This declares the password property as a string within the DTO.

username: string;: This declares the username property as a string within the DTO.

Login DTO

This DTO is used to validate the inputs that we get from the user when they log in. We use the class validator library to validate the database columns.

//login.dto.ts
import { IsEmail, IsString } from 'class-validator';

export class LoginDto {
  @IsEmail()
  email: string;

  @IsString()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

Login Response DTO

This DTO is used to supply the database columns that are to be contained in the log-in response when a user logs in. We shall see this when we test the endpoints.

//loginResponse.dto.ts
export class LoginResponseDto {
  username: string;
  email: string;
}
Enter fullscreen mode Exit fullscreen mode

User service

A service is used to manage the business logic and data manipulation from the repository. It helps with the separation of concerns and code reusability.

We shall add the user service to the services folder as we shall have the functions that create and sign in a new user.

//user.service.ts
import { Injectable } from '@nestjs/common';
import { User } from '../entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { SignupDto } from '../dto/signup.dto';
import * as bcrypt from 'bcrypt';
import { LoginDto } from '../dto/login.dto';
import { LoginResponseDto } from '../dto/loginResponse.dto';
import { Repository } from 'typeorm';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  findById(id: string): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }

  private async hashPassword(password: string, salt: string): Promise<string> {
    return bcrypt.hash(password, salt);
  }

  async create(signupDto: SignupDto): Promise<User> {
    const { email, password, username } = signupDto;
    const user = new User();

    user.salt = await bcrypt.genSalt();
    user.password = await this.hashPassword(password, user.salt);
    user.email = email;
    user.username = username;

    try {
      await user.save();
      return user;
    } catch (error) {
      throw error;
    }
  }

  async signIn(loginDto: LoginDto): Promise<LoginResponseDto> {
    const { email, password } = loginDto;
    const user = await this.userRepository.findOne({ where: { email } });

    if (user && user.validatePassword(password)) {
      const userResponse = new LoginResponseDto();

      userResponse.username = user.username;
      userResponse.email = user.email;
      return userResponse;
    } else {
      return null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's discuss this code:

@Injectable(): This decorator indicates that the UserService class can be injected with dependencies. It's a crucial part of the Dependency Injection system in NestJS.

constructor(@InjectRepository(User) private userRepository: Repository) { ... }: This constructor injects the userRepository as a dependency into the UserService class. The @InjectRepository decorator is part of TypeORM and injects the repository instance associated with the User entity.

findById(id: string): Promise: This method queries the database using the injected userRepository to find a user by their id.

private async hashPassword(password: string, salt: string): Promise: This is a private method that asynchronously hashes a password using the bcrypt library. The salt is passed as an additional parameter for added security.

async create(signupDto: SignupDto): Promise: This method creates a new user based on the provided SignupDto and persists it to the database. It generates a unique salt, hashes the password, and then saves the user entity to the database.

async signIn(loginDto: LoginDto): Promise: This method handles user authentication. It attempts to find a user by email from the database, and if found, it validates the password using the validatePassword method defined in the User entity. If the credentials are valid, a LoginResponseDto is returned, containing the user's username and email.

Configuring Passport JS and JWT

Passport JS is an authentication middleware for Node JS applications. JSON Web Tokens(JWT) is a token-based authentication system that uses an encrypted token to manage user authentication.

To configure the two for user authentication we begin by installing the relevant packages as shown below.

npm install @nestjs/passport @nestjs/jwt passport-jwt

We then create a constants file that shall contain an object that stores the JWT secret.

//constants.ts
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config();

export const jwtConstants = {
  secret: process.env.JWT_SECRET,
};
Enter fullscreen mode Exit fullscreen mode

export const jwtConstants = { secret: process.env.JWT_SECRET };: This line of code exports an object named jwtConstants that contains the JWT secret.

The JWT secret is used to sign and verify JWT tokens. In this case, the secret is retrieved from the environment variables using process.env.JWT_SECRET, which means you should have a variable named JWT_SECRET defined in your .env file.

Auth Module

We add the JWT and Passport JS modules to the auth module at the imports array. or the passport module, configure the default strategy to be JWT.

For the JWT module configure the secrets with the jwtConstants object. The signOptions is where we declare the lifetime of a town. The shorter the better to ensure user tokens are refreshed often as a security measure.

//auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './services/auth.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UserService } from './services/user.service';

@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: {
        expiresIn: '1h',
      },
    }),
  ],
  controllers: [AuthController],
  providers: [UserService, AuthService, JwtStrategy],
  exports: [JwtModule, PassportModule],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Adding JWT to the auth service

After configuring passport and JWT modules we proceed to use them in the auth service.

We begin by creating a user-jwt interface. An interface in TypeScript is a way to define the structure of an object, specifying what properties it should have and their types.

//user-jwt.interface.ts
import { LoginResponseDto } from '../dto/loginResponse.dto';

export interface UserJwtResponse {
  user: LoginResponseDto;
  accessToken: string;
}
Enter fullscreen mode Exit fullscreen mode

export interface UserJwtResponse { ... }: This line defines an interface named UserJwtResponse. Here's what's defined within the UserJwtResponse interface:

user: LoginResponseDto;: This property specifies that the user property of the UserJwtResponse object should be of type LoginResponseDto. It suggests that this property will contain data in the structure defined by the LoginResponseDto class.

accessToken: string;: This property specifies that the accessToken property of the UserJwtResponse object should be of type string. This property is intended to hold a JSON Web Token (JWT) access token.

Authentication service

The auth service encapsulates user authentication logic within a NestJS application. It provides methods for validating users by ID, handling user registration, and managing user login.

//auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from '../services/user.service';
import { SignupDto } from '../dto/signup.dto';
import { User } from '../entities/user.entity';
import { UserJwtResponse } from '../interfaces/user-jwt.interface';
import { LoginDto } from '../dto/login.dto';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) {}
  async validateUserById(userId: string) {
    return await this.userService.findById(userId);
  }

  async signUp(signupDto: SignupDto): Promise<User> {
    return this.userService.create(signupDto);
  }

  async login(loginDto: LoginDto): Promise<UserJwtResponse> {
    const userResult = await this.userService.signIn(loginDto);

    if (!userResult) {
      throw new UnauthorizedException('Invalid Credentials!');
    }

    const payload = { userResult };
    const accessToken = await this.jwtService.sign(payload);

    const signInResponse: UserJwtResponse = { user: userResult, accessToken };

    return signInResponse;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let us discuss the code:

@Injectable(): This decorator indicates that the AuthService class can be injected with dependencies. This is a key feature of NestJS's Dependency Injection system.

constructor(...): The constructor of the AuthService class receives two dependencies: userService and jwtService. These dependencies are automatically injected when an instance of AuthService is created.

async validateUserById(userId: string) { ... }: This method is used to validate a user based on their userId. It uses the injected userService to find and return user information.

async signUp(signupDto: SignupDto): Promise { ... }: This method is used to handle user registration. It takes a signupDto (presumably containing user signup information) and delegates the registration process to the userService. It returns the created user entity.

async login(loginDto: LoginDto): Promise { ... }: This method handles user login. It takes a loginDto (presumably containing user login credentials) and proceeds as follows:

It calls the signIn method of the userService to attempt user authentication.

If authentication is successful, it generates an access token using the injected jwtService.

It constructs a response object of type UserJwtResponse containing the authenticated user's information and the generated access token.

If authentication fails (no user found or invalid credentials), it throws an UnauthorizedException.

The signIn method is likely defined in the userService and handles user authentication, possibly returning the authenticated user's data or null if authentication fails.

Jwt strategy

The JWT strategy class defines a JWT-based authentication strategy using Passport in a NestJS application. This strategy is an essential component of JWT-based authentication in the application.

//jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from '../services/auth.service';
import { jwtConstants } from '../constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return await this.authService.validateUserById(payload.sub);
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's discuss the code:

@Injectable(): This decorator indicates that the JwtStrategy class can be injected with dependencies. It's important for Dependency Injection to work properly.

export class JwtStrategy extends PassportStrategy(Strategy) { ... }: This line defines the JwtStrategy class, which extends PassportStrategy from the @nestjs/passport module. The Strategy parameter passed to PassportStrategy specifies the type of authentication strategy being implemented.

constructor(private authService: AuthService) { ... }: The constructor of the JwtStrategy class receives an instance of the AuthService as a dependency. This dependency is automatically injected when an instance of JwtStrategy is created.

super({ ... }): This line calls the constructor of the parent class (PassportStrategy) and provides options for configuring the JWT strategy. Specifically:

jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(): This specifies that the JWT will be extracted from the Authorization header as a Bearer token.

secretOrKey: jwtConstants.secret: This uses the JWT secret from the jwtConstants object (likely defined elsewhere) for token verification.

async validate(payload: any) { ... }: This method is the heart of the JWT strategy. It's called when a JWT is successfully decoded and validated. The payload parameter contains the information stored in the JWT payload, including the user's ID (sub).

Inside this method, the validateUserById method of the injected authService is called with the user's ID extracted from the payload.

The validateUserById method is expected to return a user entity or null, indicating whether the user exists or not.

Authentication controller

The auth controller is responsible for handling authentication-related HTTP requests. It delegates the actual authentication and registration logic to the methods of the injected auth service.

//auth.controller.ts
import { Body, Controller, Post, Put } from '@nestjs/common';
import { AuthService } from './services/auth.service';
import { SignupDto } from './dto/signup.dto';
import { User } from './entities/user.entity';
import { LoginDto } from './dto/login.dto';
import { UserJwtResponse } from './interfaces/user-jwt.interface';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('signup')
  async signup(@Body() signupDto: SignupDto): Promise<User> {
    return this.authService.signUp(signupDto);
  }

  @Put('login')
  async login(@Body() loginDto: LoginDto): Promise<UserJwtResponse> {
    return this.authService.login(loginDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Let us discuss the code:

@Controller('auth'): This decorator sets the base route for the AuthController to 'auth', which means that the routes defined within this controller will be accessed under the /auth endpoint.

export class AuthController { ... }: This line defines the AuthController class.

constructor(private readonly authService: AuthService) { ... }: The constructor of the AuthController class receives an instance of the AuthService as a dependency. This allows the controller to use methods and functionality provided by the AuthService.

@post('signup'): This decorator specifies that the signup method will handle HTTP POST requests to the /auth/signup endpoint.

async signup(@body() signupDto: SignupDto): Promise { ... }: This method is responsible for user registration. It receives a signupDto containing user registration data from the request body.

The signupDto is passed to the signUp method of the injected authService, which handles the user registration process.

The method returns a Promise which presumably represents the created user entity.

@Put('login'): This decorator specifies that the login method will handle HTTP PUT requests to the /auth/login endpoint.

async login(@body() loginDto: LoginDto): Promise { ... }: This method is responsible for user login. It receives a loginDto containing user login credentials from the request body.

The loginDto is passed to the login method of the injected authService, which handles the user authentication process.

If authentication is successful, the method returns a Promise containing user information and an access token.

Running a migration

Before testing the endpoints we need to create and run a migration to update the database schema with the user's table and columns.

The command we shall use to create a migration is as shown below. It matches what we have configured in the datasource file.

npm run typeorm:generate-migration migrations/CreateAuthTable

Then we follow that command with another one that runs the migration.

npm run typeorm:run-migrations

Testing the endpoints

After successfully running the migration, we proceed to test the endpoints. Run the API using this command as it will run the API in watch mode.

npm run start:dev

Sign up endpoint

In Postman, we create the body of the request we want to make which will create a new user.

Signup endpoint

Login endpoint

In Postman, we create the body of the request we shall use to log in the user we created.

Login endpoint

The GitHub repository with the code for this project is here. Feel free to comment with any questions you may have. Until next time, may the code be with you.

Top comments (2)

Collapse
 
joset98 profile image
joset98

I think you should create access_token in signup endpoint to redirect directly to main app

Collapse
 
andisiambuku profile image
Andisi (Roseland) Ambuku

oh yeah, thanks