loading...

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

darioielardi profile image Dario Ielardi Originally published at darioielardi.hashnode.dev ・11 min read

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

We can now generate the guard in the same module.

nest generate guard auth/simple

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;
  }
}

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;
  }
}

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();
  }
}

Let's test this out.

http localhost:3000/hello
HTTP/1.1 401 Unauthorized

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

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

Hello World!

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

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',
};

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 {}

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],
  // ...

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;
}
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,
    };
  }
}

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
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);
  }
}

Let's test this out:

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

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

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

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;
  }
}

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],
  // ...

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 {
// ...

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
HTTP/1.1 401 Unauthorized

{
  "message": "Unauthorized",
  "statusCode": 401
}

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"
HTTP/1.1 201 Created

{
  "token": "<auth token>",
  "user": {...}
}

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

export TOKEN="<your token here>"

You can now use it for every request like this:

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

{
  "displayName": "Jack",
  "password": "123456",
  "username": "jack"
}

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;
  }

// ...

And let's test it.

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

{
  "displayName": "Jack",
  "username": "jack",
}

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;

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;
  }

// ...

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;
}

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 {}

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 {}

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,
    };
  }
}

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);
  }
  // ...
}

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"
HTTP/1.1 201 Created

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

{
  "displayName": "Mary",
  "username": "mary"
}

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

Posted on by:

darioielardi profile

Dario Ielardi

@darioielardi

CTO & Founder @myothis. I have several years of experience as a fullstack developer. I mostly work with NodeJS, Go, Postgres, React and Flutter.

Discussion

markdown guide