DEV Community

Dario Ielardi
Dario Ielardi

Posted on • Originally published at darioielardi.hashnode.dev

How To Build A Twitter Clone With NestJS, Prisma And React ( Part 2 )

This tutorial has been originally published on my Hashnode blog.


Authentication

There are a lot of different authentication strategies to protect our API endpoints.

Generally, I strongly suggest to delegate such a crucial feature to a dedicated service such as Firebase Authentication, AWS Cognito or Auth0.
However, today we're going to build a basic and incomplete authentication system to understand how Nest approaches the problem.

Let me say that again: this is not a complete solution, it's far from being secure and production-ready since it lacks a lot of essential features for a good authentication system.
We just want to explore the possibilities Nest gives us to implement authentication in our server and how it can integrate existing solutions.

The authentication system we are going to build is based on JSON Web Tokens ( JWT ). Those are essentially a standard and secure way to transmit information over the network, encrypted and signed by your server to be verified on every request.

The authentication flow is basically this:

  1. A user will ask for a JWT sending a request to the auth/login endpoint with his username and password in the request body.
  2. If that information is correct, the server will generate, encrypt and send back a signed JWT, which will carry the username and will have an expiration time.
  3. On every subsequent request, the user will send the received JWT in the Authorization header, which will be verified by the server. If the token is valid and the expiration time has not passed, the server will proceed to handle the request, and it will know which user made it thanks to the username stored in the JWT.

Sending the access token for every request very much exposes it to man-in-the-middle attacks, that's why this authentication system usually requires a very short token expiration time and a mechanism to refresh the token.
Since this is beyond the scope of this tutorial, we will set an expiration time of one hour, after which the user will need to ask for another token sending his username and password to the auth/login endpoint again.

To learn more about JWT you can read this well-crafted introduction.

Guards

Nest provides a very versatile element to handle endpoints protection: guards.

A guard is just an Injectable class which implements the CanActivate interface. It can be applied to any endpoint or a whole controller class.

Guards do not enforce a particular authentication strategy, they are just used to tell Nest to run some code before the request gets passed to the handler method.

To implement our first guard let's first generate the auth module.

nest generate module auth
nest generate service auth
Enter fullscreen mode Exit fullscreen mode

We can now generate the guard in the same module.

nest generate guard auth/simple
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the generated file.

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class SimpleGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see the only thing we need here is the canActivate method.
When this guard is applied to an endpoint or a controller, Nest calls the canActivate method before every request, and, based on its boolean return value, it either passes the request to the controller or returns a 403 Forbidden response. Of course, we can throw any other exception and it will be caught and send back to the client.

The most powerful feature of this method is that it can access the request object, thanks to its context argument.

Let's update this guard to check the presence of an MY_AUTH_TOKEN string in the Authorization header.

// ...
export class SimpleGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    const req: Request = context.switchToHttp().getRequest();

    const token = req.headers['authorization'];

    if (!token) {
      throw new UnauthorizedException('token_not_found');
    }

    if (token !== 'MY_AUTH_TOKEN') {
      throw new UnauthorizedException('invalid_token');
    }

    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

To apply this guard to an endpoint or a controller we can use the UseGuards decorator. Let's do that with the getHello method in the AppController.

// src/app.controller.ts

import {
  // ...
  UseGuards,
} from '@nestjs/common';
import { SimpleGuard } from './auth/simple.guard';
// ...

@Controller()
export class AppController {
  // ...

  @UseGuards(SimpleGuard)
  @Get('hello')
  getHello(): string {
    return this.appService.getHello();
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's test this out.

http localhost:3000/hello
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 401 Unauthorized

{
  "error": "Unauthorized",
  "message": "token_not_found",
  "statusCode": 401
}
Enter fullscreen mode Exit fullscreen mode
http localhost:3000/hello Authorization:"INVALID_TOKEN"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 401 Unauthorized

{
  "error": "Unauthorized",
  "message": "invalid_token",
  "statusCode": 401
}
Enter fullscreen mode Exit fullscreen mode
http localhost:3000/hello Authorization:"MY_AUTH_TOKEN"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 200 OK

Hello World!
Enter fullscreen mode Exit fullscreen mode

We now know what a guard is and how to use it.

However, to implement our authentication system we are not going to write a guard, and that's because someone already wrote one for us.

Passport

Nest provides us an additional module to integrate with passport, the most popular and mature NodeJS authentication library.

Passport acts as a toolset capable of handling a lot of different authentication strategies. The key to make it work in a Nest application is, once again, to encapsulate the one we need in an injectable service. Once we do that, we can use a built-in guard exported by the @nestjs/passport library to let passport do its work for every incoming request.

Let's install everything we need.

npm install @nestjs/passport passport @nestjs/jwt passport-jwt
npm install @types/passport-jwt --save-dev
Enter fullscreen mode Exit fullscreen mode

As you can see, we also installed @nestjs/jwt, which is a utility package to manipulate JWTs, thanks to the jsonwebtoken library which it encapsulates.

We will now need some JWT configuration constants which we can store in the auth/jwt.constants.ts file.

export const jwtConstants = {
  secret: 'secretKey',
};
Enter fullscreen mode Exit fullscreen mode

The secret field is going to be used by passport to sign and verify every generated JWT. We usually want to provide a more robust and complicated secret.

Next, we are going to import the PassportModule and JwtModule provided by the @nestjs/passport and @nestjs/jwt packages in our AuthModule's imports.

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { jwtConstants } from './jwt.constants';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '1h' },
    }),
  ],
  providers: [AuthService],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

The JwtModule.register is a sort of factory to allow us to provide some configuration to the JwtModule. This technique is pretty frequent in the NestJS world, and we refer to it as dynamic modules.

To be able to access the database in the AuthService we now need to import our PrismaService in the AuthModule.providers field.

// ...
import { PrismaService } from '../prisma.service';
// ...
@Module({
  // ...
  providers: [AuthService, PrismaService],
  // ...
Enter fullscreen mode Exit fullscreen mode

Next, we will create an auth.dto.ts file with a LoginDto class and an AuthResponse, and in our AuthService class we will implement the login method.
This method will then:

  1. Check if a user with the provided username really exists.
  2. Validate the password using the bcrypt library, comparing it with the hash in our database.
  3. Generate and return a signed JWT along with the user object.
// auth.dto.ts

import { IsString, Length } from 'class-validator';
import { User } from '@prisma/client';

export class LoginDto {
  @IsString()
  @Length(3, 30)
  username: string;

  @IsString()
  @Length(6, 30)
  password: string;
}

export class AuthResponse {
  token: string;
  user: User;
}
Enter fullscreen mode Exit fullscreen mode
import {
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma.service';
import { LoginDto } from './auth.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private db: PrismaService, private jwt: JwtService) {}

  async login(data: LoginDto): Promise<AuthResponse> {
    const { username, password } = data;

    const user = await this.db.user.findOne({
      where: { username },
    });

    if (!user) {
      throw new NotFoundException();
    }

    const passwordValid = await bcrypt.compare(password, user.password);

    if (!passwordValid) {
      throw new UnauthorizedException('invalid_password');
    }

    delete user.password;

    return {
      token: this.jwt.sign({ username }),
      user,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Everything here is pretty clear. Notice how we asked Nest to inject the JwtService from the @nestjs/jwt package to be used inside our class.
This is only possible because the JwtService is an exported provider in the JwtModule we imported in the AuthModule. We'll see how this mechanism works with a local module later on.

We can now generate our auth controller and implement the auth/login endpoint.

nest generate controller auth
Enter fullscreen mode Exit fullscreen mode
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto, AuthResponse } from './auth.dto';

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

  @Post('login')
  login(@Body() data: LoginDto): Promise<AuthResponse> {
    return this.service.login(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's test this out:

http POST localhost:3000/auth/login username="jack" password="invalid"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 401 Unauthorized

{
  "error": "Unauthorized",
  "message": "invalid password",
  "statusCode": 401
}
Enter fullscreen mode Exit fullscreen mode
http POST localhost:3000/auth/login username="jack" password="123456"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 201 Created

{
  "token": "<a very long token>",
  "user": {
    "username": "jack",
    "displayName": "Jack"
  }
}
Enter fullscreen mode Exit fullscreen mode

It definitely seems to work.

We now need to implement a strategy, extending the default one exported by passport-jwt, which will make passport able to verify the JWT on every request.

Let's create the auth/jwt.strategy.ts file.

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './jwt.constants';
import { PrismaService } from '../prisma.service';

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

  async validate(payload: { username: string }) {
    const user = await this.db.user.findOne({
      where: { username: payload.username },
    });

    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's analyze what we're doing here:

  • We're creating an injectable class extending the passport strategy exported from passport-jwt and wrapped by the PassportStragey utility function exported by @nestjs/passport.
  • We're passing some configuration data to the strategy constructor, and injecting the PrismaService at the same time.
  • The validate method will only be called by passport when a valid JWT has been found in the Authorization header. The return value of this method will be attached to the request object by passport, and will be accessible in every controller handler as request.user. Therefore we just need to fetch the user from the database and return it.

We can now add this new strategy class to the providers list of the AuthModule.

// auth.module.ts

// ..
import { JwtStrategy } from './jwt.strategy';

@Module({
  // ...
  providers: [AuthService, PrismaService, JwtStrategy],
  // ...
Enter fullscreen mode Exit fullscreen mode

We are now ready to apply our JWT authentication system to our endpoints through a guard.

The @nestjs/passport module exports a built-in AuthGuard to be used in our UseGuards decorator. Let's do that with our UsersController.

// users.controller.ts

import {
  // ...
  UseGuards
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@UseGuards(AuthGuard('jwt'))
@Controller('users')
export class UsersController {
// ...
Enter fullscreen mode Exit fullscreen mode

Passing the jwt string parameter, Nest will look for a provider class anywhere among our application's dependencies which extends the Strategy exported by the passport-jwt strategy, and it will find our JwtStrategy class.

Every endpoint in this controller is now protected. Let's test this out.

http localhost:3000/users/jack
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 401 Unauthorized

{
  "message": "Unauthorized",
  "statusCode": 401
}
Enter fullscreen mode Exit fullscreen mode

As we can see, without an authentication token in the Authorization header we always receive a 401 error. Let's get one with our auth/login endpoint.

http POST localhost:3000/auth/login username="jack" password="123456"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 201 Created

{
  "token": "<auth token>",
  "user": {...}
}
Enter fullscreen mode Exit fullscreen mode

Just copy the received token and export it in an environment variable like this:

export TOKEN="<your token here>"
Enter fullscreen mode Exit fullscreen mode

You can now use it for every request like this:

http localhost:3000/users/jack Authorization:"Bearer $TOKEN"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 200 OK

{
  "displayName": "Jack",
  "password": "123456",
  "username": "jack"
}
Enter fullscreen mode Exit fullscreen mode

Let's now see how we can access the authenticated user in a handler method.

Custom decorators

As we already know, the JwtStrategy takes care of attaching the result of the validate function in the request object, which is the user we fetched from the database.

The request object is the same you may know if you ever used the express framework, which Nest is based on and which we got already installed by the Nest CLI.
To access it in a controller method we can use the Req decorator.
Let's implement a new protected endpoint auth/me to demonstrate that.

// auth.controller.ts

import {
  // ...
  Get,
  UseGuards,
  Req,
} from '@nestjs/common';
import { User } from '@prisma/client';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';

// ...

  @UseGuards(AuthGuard('jwt'))
  @Get('me')
  me(@Req() req: Request): User {
    const user = req.user as User;
    delete user.password;
    return user;
  }

// ...
Enter fullscreen mode Exit fullscreen mode

And let's test it.

http localhost:3000/auth/me Authorization:"Bearer $TOKEN"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 200 OK

{
  "displayName": "Jack",
  "username": "jack",
}
Enter fullscreen mode Exit fullscreen mode

As we can see there is something pretty disturbing in this implementation.
Every time we need to access the user object we have to cast it to the right User type and eventually remove the password field, which will become annoying as soon as our application grows.
This is a perfect use case for a custom decorator.

Let's create a new file src/common/decorators/auth-user.decorator.ts.

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { User } from '@prisma/client';

const AuthUser = createParamDecorator((_, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  const user = request.user as User;
  delete user.password;
  return user;
});

export default AuthUser;
Enter fullscreen mode Exit fullscreen mode

While for a simple class or function decorator we could simply use the Typescript syntax, Nest provides us a createParamDecorator utility specifically for arguments of controllers' handlers.
We provide a function as the only argument, whose second argument is the server ExecutionContext, from which we can get the request object.

Now we can replace the Req decorator with our new AuthUser decorator in the me handler.

// auth.controller.ts

// ...
import AuthUser from '../common/decorators/auth-user.decorator';
// ...

  @UseGuards(AuthGuard('jwt'))
  @Get('me')
  me(@AuthUser() user: User): User {
    return user;
  }

// ...
Enter fullscreen mode Exit fullscreen mode

Custom decorators are a very powerful feature of Nest. More on that in the dedicated page of the Nest documentation.

User registration

The last thing we need to handle is user registration.
Right now is barely implemented in the UsersController, but we want to properly implement it in the AuthController as a new auth/register endpoint.

After the new user has been created we should generate and send back a JWT to let him authenticate on subsequent requests, without the need to call the auth/login endpoint.

Let's add a new RegisterDto class to the auth.dto.ts file, identical to the CreateUserDto ( you can actually copy that ).

// auth.dto.ts

// ...
export class RegisterDto {
  @IsString()
  @Length(3, 30)
  username: string;

  @IsString()
  @Length(6, 30)
  password: string;

  @IsString()
  @Length(1, 50)
  displayName: string;
}
Enter fullscreen mode Exit fullscreen mode

We can now implement our register method in the AuthService, and to do that we want to take advantage of the create method we have in the UsersService.
This means the UsersModule has to expose that feature exporting the UsersService to be used by other modules.
To do that we just need to add an exports field to the Module decorator of the UsersModule, and put the UsersService inside.

// ...
import { UsersService } from './users.service';

@Module({
  // ...
  exports: [UsersService],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

This way, any other module can import the UsersModule to take advantage of any of the exported classes.

Let's do that with the AuthModule.

// ...
import { UsersModule } from '../users/users.module';

@Module({
  imports: [
    UsersModule,
    // ...
  ],
  // ...
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Now, thanks to the power of Nest, we can easily inject the UsersService into the AuthService and implement our register method.

import { LoginDto, RegisterDto, AuthResponse } from './auth.dto';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    // ...
    private users: UsersService
  ) {}
  // ...
  async register(data: RegisterDto): Promise<AuthResponse> {
    const user = await this.users.create(data);
    return {
      token: this.jwt.sign({ username: user.username }),
      user,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's now wire our new method to the corresponding auth/register endpoint.

// ...
import { LoginDto, RegisterDto, AuthResponse } from './auth.dto';

@Controller('auth')
export class AuthController {
  // ...
  @Post('register')
  register(@Body() data: RegisterDto): Promise<AuthResponse> {
    return this.service.register(data);
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Finally, we just need to clean everything up removing the create method from the UsersController.

Let's test the new auth/register endpoint.

http POST localhost:3000/auth/register username="mary" displayName="Mary" password="secret"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 201 Created

{
  "token": "<generated code>",
  "user": {
    "username": "mary",
    "displayName": "Mary"
  }
}
Enter fullscreen mode Exit fullscreen mode
export TOKEN="<our new token>"
http localhost:3000/auth/me Authorization:"Bearer $TOKEN"
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 200 OK

{
  "displayName": "Mary",
  "username": "mary"
}
Enter fullscreen mode Exit fullscreen mode

We are now ready to implement our main application feature: tweets.

Top comments (0)