March 31, 2023

Building a REST API with NestJS and Prisma: Authentication

10 min read

Welcome to the fifth tutorial in this series about building a REST API with NestJS, Prisma and PostgreSQL! In this tutorial, you will learn how to implement JWT authentication in your NestJS REST API.

Building a REST API with NestJS and Prisma: Authentication

Table Of Contents

Introduction

In the previous chapter of this series, you learned how to handle relational data in your NestJS REST API. You created a User model and added a one-to-many relationship between User and Article models. You also implemented the CRUD endpoints for the User model.

In this chapter, you will learn how to add authentication to your API using a package called Passport:

  1. First, you will implement JSON Web Token (JWT) based authentication using a library called Passport.
  2. Next, you will protect the passwords stored in your database by hashing them using the bcrypt library.

In this tutorial, you will use the API built in the last chapter.

Development environment

To follow along with this tutorial, you will be expected to:

  • ... have Node.js installed.
  • ... have Docker and Docker Compose installed. If you are using Linux, please make sure your Docker version is 20.10.0 or higher. You can check your Docker version by running docker version in the terminal.
  • ... optionally have the Prisma VS Code Extension installed. The Prisma VS Code extension adds some really nice IntelliSense and syntax highlighting for Prisma.
  • ... optionally have access to a Unix shell (like the terminal/shell in Linux and macOS) to run the commands provided in this series.

If you don't have a Unix shell (for example, you are on a Windows machine), you can still follow along, but the shell commands may need to be modified for your machine.

Clone the repository

The starting point for this tutorial is the ending of chapter two of this series. It contains a rudimentary REST API built with NestJS.

The starting point for this tutorial is available in the end-validation branch of the GitHub repository. To get started, clone the repository and checkout the end-validation branch:

Now, perform the following actions to get started:

  1. Navigate to the cloned directory:
  1. Install dependencies:
  1. Start the PostgreSQL database with Docker:
  1. Apply database migrations:
  1. Start the project:

Note: Step 4 will also generate Prisma Client and seed the database.

Now, you should be able to access the API documentation at http://localhost:3000/api/.

Project structure and files

The repository you cloned should have the following structure:

Note: You might notice that this folder comes with a test directory as well. Testing won't be covered in this tutorial. However, if you want to learn about how best practices for testing your applications with Prisma, be sure to check out this tutorial series: The Ultimate Guide to Testing with Prisma

The notable files and directories in this repository are:

  • The src directory contains the source code for the application. There are three modules:
    • The app module is situated in the root of the src directory and is the entry point of the application. It is responsible for starting the web server.
    • The prisma module contains Prisma Client, your interface to the database.
    • The articles module defines the endpoints for the /articles route and accompanying business logic.
    • The users module defines the endpoints for the /users route and accompanying business logic.
  • The prisma folder has the following:
    • The schema.prisma file defines the database schema.
    • The migrations directory contains the database migration history.
    • The seed.ts file contains a script to seed your development database with dummy data.
  • The docker-compose.yml file defines the Docker image for your PostgreSQL database.
  • The .env file contains the database connection string for your PostgreSQL database.

Note: For more information about these components, go through chapter one of this tutorial series.

Implement authentication in your REST API

In this section, you will implement the bulk of the authentication logic for your REST API. By the end of this section, the following endpoints will be auth protected 🔒:

  • GET /users
  • GET /users/:id
  • PATCH /users/:id
  • DELETE /users/:id

There are two main types of authentication used on the web: session-based authentication and token-based authentication. In this tutorial, you will implement token-based authentication using JSON Web Tokens (JWT).

Note: This short video explains the basics of both kinds of authentication.

To get started, create a new auth module in your application. Run the following command to generate a new module:

You will be given a few CLI prompts. Answer the questions accordingly:

  1. What name would you like to use for this resource (plural, e.g., "users")? auth
  2. What transport layer do you use? REST API
  3. Would you like to generate CRUD entry points? No

You should now find a new auth module in the src/auth directory.

Install and configure passport

passport is a popular authentication library for Node.js applications. It is highly configurable and supports a wide range of authentication strategies. It is meant to be used with the Express web framework, which NestJS is built on. NestJS has a first-party integration with passport called @nestjs/passport that makes it easy to use in your NestJS application.

Get started by installing the following packages:

Now that you have installed the required packages, you can configure passport in your application. Open the src/auth.module.ts file and add the following code:

The @nestjs/passport module provides a PassportModule that you can import into your application. The PassportModule is a wrapper around the passport library that provides NestJS specific utilities. You can read more about the PassportModule in the official documentation.

You also configured a JwtModule that you will use to generate and verify JWTs. The JwtModule is a wrapper around the jsonwebtoken library. The secret provides a secret key that is used to sign the JWTs. The expiresIn object defines the expiration time of the JWTs. It is currently set to 5 minutes.

Note: Remember to generate a new token if the previous one has expired.

You can use the jwtSecret shown in the code snippet or generate your own using OpenSSL.

Note: In a real application, you should never store the secret directly in your codebase. NestJS provides the @nestjs/config package for loading secrets from environment variables. You can read more about it in the official documentation.

Implement a POST /auth/login endpoint

The POST /login endpoint will be used to authenticate users. It will accept a username and password and return a JWT if the credentials are valid. First you create a LoginDto class that will define the shape of the request body.

Create a new file called login.dto.ts inside the src/auth/dto directory:

Now define the LoginDto class with a email and password field:

You will also need to define a new AuthEntity that will describe the shape of the JWT payload. Create a new file called auth.entity.ts inside the src/auth/entity directory:

Now define the AuthEntity in this file:

The AuthEntity just has a single string field called accessToken, which will contain the JWT.

Now create a new login method inside AuthService:

The login method first fetches a user with the given email. If no user is found, it throws a NotFoundException. If a user is found, it checks if the password is correct. If the password is incorrect, it throws a UnauthorizedException. If the password is correct, it generates a JWT containing the user's ID and returns it.

Now create the POST /auth/login method inside AuthController:

Now you should have a new POST /auth/login endpoint in your API.

Go to the http://localhost:3000/api page and try the POST /auth/login endpoint. Provide the credentials of a user that you created in your seed script

You can use the following request body:

After executing the request you should get a JWT in the response.

POST /auth/login endpoint

In the next section, you will use this token to authenticate users.

Implement JWT authentication strategy

In Passport, a strategy is responsible for authenticating requests, which it accomplishes by implementing an authentication mechanism. In this section, you will implement a JWT authentication strategy that will be used to authenticate users.

You will not be using the passport package directly, but rather interact with the wrapper package @nestjs/passport, which will call the passport package under the hood. To configure a strategy with @nestjs/passport, you need to create a class that extends the PassportStrategy class. You will need to do two main things in this class:

  1. You will pass JWT strategy specific options and configuration to the super() method in the constructor.
  2. A validate() callback method that will interact with your database to fetch a user based on the JWT payload. If a user is found, the validate() method is expected to return the user object.

First create a new file called jwt.strategy.ts inside the src/auth/strategy directory:

Now implement the JwtStrategy class:

You have created a JwtStrategy class that extends the PassportStrategy class. The PassportStrategy class takes two arguments: a strategy implementation and the name of the strategy. Here you are using a predefined strategy from the passport-jwt library.

You are passing some options to the super() method in the constructor. The jwtFromRequest option expects a method that can be used to extract the JWT from the request. In this case, you will use the standard approach of supplying a bearer token in the Authorization header of our API requests. The secretOrKey option tells the strategy what secret to use to verify the JWT. There are many more options, which you can read about in the passport-jwt repository..

For the passport-jwt, Passport first verifies the JWT's signature and decodes the JSON. The decoded JSON is then passed to the validate() method. Based on the way JWT signing works, you're guaranteed receiving a valid token that was previously signed and issued by your app. The validate() method is expected to return a user object. If the user is not found, the validate() method throws an error.

Note: Passport can be quite confusing. It's helpful to think of Passport as a mini framework in itself that abstracts the authentication process into a few steps that can be customized with strategies and configuration options. I reccomend reading the NestJS Passport recipe to learn more about how to use Passport with NestJS.

Add the new JwtStrategy as a provider in the AuthModule:

Now the JwtStrategy can be used by other modules. You have also added the UsersModule in the imports, because the UsersService is being used in the JwtStrategy class.

To make UsersService accessible in the JwtStrategy class, you also need to add it in the exports of the UsersModule:

Implement JWT auth guard

Guards are a NestJS construct that determines whether a request should be allowed to proceed or not. In this section, you will implement a custom JwtAuthGuard that will be used to protect routes that require authentication.

Create a new file called jwt-auth.guard.ts inside the src/auth directory:

Now implement the JwtAuthGuard class:

The AuthGuard class expects the name of a strategy. In this case, you are using the JwtStrategy that you implemented in the previous section, which is named jwt.

You can now use this guard as a decorator to protect your endpoints. Add the JwtAuthGuard to routes in the UsersController:

If you try to query any of these endpoints without authentication it will no longer work.

`GET /users endpoint gives 401 response

Integrate authentication in Swagger

Currently there's no indication on Swagger that these endpoints are auth protected. You can add a @ApiBearerAuth() decorator to the controller to indicate that authentication is required:

Now, auth protected endpoints should have a lock icon in Swagger 🔓

Auth protected endpoints in Swagger

It's currently not possible to "authenticate" yourself directly in Swagger so you can test these endpoints. To do this, you can add the .addBearerAuth() method call to the SwaggerModule setup in main.ts:

You can now add a token by clicking on the Authorize button in Swagger. Swagger will add the token to your requests so you can query the protected endpoints.

Note: You can generate a token by sending a POST request to /auth/login endpoint with a valid email and password.

Try it out yourself.

Authentication workflow in Swagger

Hashing passwords

Currently, the User.password field is stored in plain text. This is a security risk because if the database is compromised, so are all the passwords. To fix this, you can hash the passwords before storing them in the database.

You can use the bcrypt cryptography library to hash passwords. Install it with npm:

First, you will update the create and update methods in the UsersService to hash the password before storing it in the database:

The bcrypt.hash function accepts two arguments: the input string to the hash function and the number of rounds of hashing (also known as cost factor). Increasing the rounds of hashing increases the time it takes to calculate the hash. There is a trade off here between security and performance. With more rounds of hashing, it takes more time to calculate the hash, which helps prevent brute force attacks. However, more rounds of hashing also mean more time to calculate the hash when a user logs in. This stack overflow answer has a good discussion on this topic.

bcrypt also automatically uses another technique called salting to make it harder to brute force the hash. Salting is a technique where a random string is added to the input string before hashing. This way, attackers cannot use a table of precomputed hashes to crack the password, as each password has a different salt value.

You also need to update your database seed script to hash the passwords before inserting them into the database:

Run the seed script with npx prisma db seed and you should see that the passwords stored in the database are now hashed.

The value of the password field will be different for you since a different salt value is used each time. The important thing is that the value is now a hashed string.

Now, if you try to use the login with the correct password, you will face a HTTP 401 error. This is because the login method tries to compare the plaintext password from the user request with the hashed password in the database. Update the login method to use hashed passwords:

You can now login with the correct password and get a JWT in the response.

Summary and final remarks

In this chapter, you learned how to implement JWT authentication in your NestJS REST API. You also learned about salting passwords and integrating authentication with Swagger.

You can find the finished code for this tutorial in the end-authentication branch of the GitHub repository. Please feel free to raise an issue in the repository or submit a PR if you notice a problem. You can also reach out to me directly on Twitter.

Don’t miss the next post!

Sign up for the Prisma Newsletter