DEV Community

Cover image for NestJS Authentication with OAuth2.0: Express Local OAuth REST API
Afonso Barracha
Afonso Barracha

Posted on • Edited on

NestJS Authentication with OAuth2.0: Express Local OAuth REST API

Series Intro

This series will cover the full implementation of OAuth2.0 Authentication in NestJS for the following types of APIs:

And it is divided in 5 parts:

  • Configuration and operations;
  • Express Local OAuth REST API;
  • Fastify Local OAuth REST API;
  • Apollo Local OAuth GraphQL API;
  • Adding External OAuth Providers to our API;

Lets start the second part of this series.

Tutorial Intro

On this tutorial we will continue building on the previous article, by creating an Express REST API.

TLDR: if you do not have 45 minutes to read the article, the code can be found on this repo

Set up

Start by creating controllers for both the auth and users:

$ nest g co auth
$ nest g co users
Enter fullscreen mode Exit fullscreen mode

Auth Module

Before implementing the endpoints we need both an auth guard, and some custom decorators:

  • Public: to make an endpoint public;
  • Origin: to get the origin from cross-origin requests:
  • CurrentUser: to get the current logged user id;

Decorators

Before creating decorators, since we will have a user field on our express Request interface install express-serve-static-core:

$ yarn add -D @types/express-serve-static-core
Enter fullscreen mode Exit fullscreen mode

And extend express on a types folder:

// express.d.ts

import { Request as ExpressRequest } from 'express';

declare module 'express-serve-static-core' {
  interface Request extends ExpressRequest {
    user?: number;
  }
}
Enter fullscreen mode Exit fullscreen mode

To be able to create a global auth guard we need to create the Public decorator first, this decorator can be found here in the docs, on the decorators folder add:

// public.decorator.ts

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

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Enter fullscreen mode Exit fullscreen mode

To validate that our tokens come from the correct origin we need to get the domain from the the front-end:

// origin.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express-serve-static-core';

export const Origin = createParamDecorator(
  (_, context: ExecutionContext): string | undefined => {
    return context.switchToHttp().getRequest<Request>().headers?.origin;
  },
);

Enter fullscreen mode Exit fullscreen mode

Finally we need to be able to get the current logged user ID:

// current-user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express-serve-static-core';

export const CurrentUser = createParamDecorator(
  (_, context: ExecutionContext): number | undefined => {
    return context.switchToHttp().getRequest<Request>()?.user;
  },
);
Enter fullscreen mode Exit fullscreen mode

Guards

Auth Guard

We need a global AuthGuard to protect our endpoints, on a guards folder, we can create a CanActivate guard:

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { isJWT } from 'class-validator';
import { Request } from 'express';
import { isNull, isUndefined } from '../../common/utils/validation.util';
import { TokenTypeEnum } from '../../jwt/enums/token-type.enum';
import { JwtService } from '../../jwt/jwt.service';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly jwtService: JwtService,
  ) {}

  public async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    const activate = await this.setHttpHeader(
      context.switchToHttp().getRequest(),
      isPublic,
    );

    if (!activate) {
      throw new UnauthorizedException();
    }

    return activate;
  }

  /**
   * Sets HTTP Header
   *
   * Checks if the header has a valid Bearer token, validates it and sets the User ID as the user.
   */
  private async setHttpHeader(
    req: Request,
    isPublic: boolean,
  ): Promise<boolean> {
    const auth = req.headers?.authorization;

    if (isUndefined(auth) || isNull(auth) || auth.length === 0) {
      return isPublic;
    }

    const authArr = auth.split(' ');
    const bearer = authArr[0];
    const token = authArr[1];

    if (isUndefined(bearer) || isNull(bearer) || bearer !== 'Bearer') {
      return isPublic;
    }
    if (isUndefined(token) || isNull(token) || !isJWT(token)) {
      return isPublic;
    }

    try {
      const { id } = await this.jwtService.verifyToken(
        token,
        TokenTypeEnum.ACCESS,
      );
      req.user = id;
      return true;
    } catch (_) {
      return isPublic;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally add the guard to the AppModule:

// ...
import { CacheModule, Module } from '@nestjs/common';
// ...
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/guards/auth.guard';
// ...

@Module({
  // ...
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

(Optional) Throttler Guard

To protect our auth endpoints against brute force attacks we should add rate limiting to our API, NestJS has a ThrottlerModule, start by installing it (and optionally the Redis storage for it):

$ yarn add @nestjs/throttler nestjs-throttler-storage-redis
Enter fullscreen mode Exit fullscreen mode

Add the throttler options to .env:

# ...
# Throttler config
THROTTLE_TTL=60
THROTTLE_LIMIT=20
Enter fullscreen mode Exit fullscreen mode

To the schema:

import Joi from 'joi';

export const validationSchema = Joi.object({
  // ...
  THROTTLE_TTL: Joi.number().required(),
  THROTTLE_LIMIT: Joi.number().required(),
});
Enter fullscreen mode Exit fullscreen mode

To the config.interface.ts file:

// ...
import { ThrottlerModuleOptions } from '@nestjs/throttler';
// ...

export interface IConfig {
  // ...
  throttler: ThrottlerModuleOptions;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

And on the index.ts of the config folder:

// ...
import { IConfig } from './interfaces/config.interface';
// ...

export function config(): IConfig {
  // ...

  return {
    // ...
    throttler: {
      ttl: parseInt(process.env.THROTTLE_TTL, 10),
      limit: parseInt(process.env.THROTTLE_LIMIT, 10),
    },
    // ...
  };
}
Enter fullscreen mode Exit fullscreen mode

Now add a config class for the throttler:

// throttler.config.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
  ThrottlerModuleOptions,
  ThrottlerOptionsFactory,
} from '@nestjs/throttler';
import { RedisOptions } from 'ioredis';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';

@Injectable()
export class ThrottlerConfig implements ThrottlerOptionsFactory {
  constructor(private readonly configService: ConfigService) {}

  createThrottlerOptions(): ThrottlerModuleOptions {
    return {
      ...this.configService.get<ThrottlerModuleOptions>('throttler'),
      storage: new ThrottlerStorageRedisService(
        this.configService.get<RedisOptions>('redis'),
      ),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Register the ThrottlerModule on the AuthModule:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerConfig } from '../config/throttler.config';
// ...

@Module({
  imports: [
    // ...
    ThrottlerModule.forRootAsync({
      imports: [ConfigModule],
      useClass: ThrottlerConfig,
    }),
  ],
  // ...
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Finally add the ThrottlerGuard to the auth controller:

import { Controller, UseGuards } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Controller('auth')
@UseGuards(ThrottlerGuard)
export class AuthController {}
Enter fullscreen mode Exit fullscreen mode

Controller

Mappers

Some data is private and should never be sent by the response of our API, such as the user password and the refresh token. Therefore, we need to create mapper functions that reflect what we want to send as a response of our API.

Interfaces

There are two interfaces that we need, one with the user data that we want to send and one for the Auth Result.

Auth Response User

// auth-response-user.interface.ts

export interface IAuthResponseUser {
  id: number;
  name: string;
  username: string;
  email: string;
  createdAt: string;
  updatedAt: string;
}
Enter fullscreen mode Exit fullscreen mode

Auth Response

// auth-response.interface.ts

import { IAuthResponseUser } from './auth-response-user.interface';

export interface IAuthResponse {
  user: IAuthResponseUser;
  accessToken: string;
}
Enter fullscreen mode Exit fullscreen mode

Classes

Now for the mapper we can either add a static method to our controllers or create separate classes with a map static method. I recommend the latter as it is easier to add OpenAPI specifications latter on.

Create a new folder called mappers and add the following files:

  1. auth-response-user.mapper.ts:

    import { IUser } from '../../users/interfaces/user.interface';
    import { IAuthResponseUser } from '../interfaces/auth-response-user.interface';
    
    export class AuthResponseUserMapper implements IAuthResponseUser {
      public id: number;
      public name: string;
      public username: string;
      public email: string;
      public createdAt: string;
      public updatedAt: string;
    
      constructor(values: IAuthResponseUser) {
        Object.assign(this, values);
      }
    
      public static map(user: IUser): AuthResponseUserMapper {
        return new AuthResponseUserMapper({
          id: user.id,
          name: user.name,
          username: user.username,
          email: user.email,
          createdAt: user.createdAt.toISOString(),
          updatedAt: user.updatedAt.toISOString(),
        });
      }
    }
    
  2. auth-response.mapper.ts:

    import { IAuthResponse } from '../interfaces/auth-response.interface';
    import { IAuthResult } from '../interfaces/auth-result.interface';
    import { AuthResponseUserMapper } from './auth-response-user.mapper';
    
    export class AuthResponseMapper implements IAuthResponse {
      public user: AuthResponseUserMapper;
      public accessToken: string;
    
      constructor(values: IAuthResponse) {
        Object.assign(this, values);
      }
    
      public static map(result: IAuthResult): AuthResponseMapper {
        return new AuthResponseMapper({
          user: AuthResponseUserMapper.map(result.user),
          accessToken: result.accessToken,
        });
      }
    }
    

Methods

While most of the business logic resides on the service, we still need some logic to be added here on the controller.

Start by injecting the AuthService and UserService, and by getting the COOKIE_NAME and JWT_REFRESH_TIME from the ConfigService:

import { Controller, UseGuards } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../users/users.service';
import { AuthService } from './auth.service';
// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  private readonly cookiePath = '/api/auth';
  private readonly cookieName: string;
  private readonly refreshTime: number;
  private readonly testing: boolean;

  constructor(
    private readonly authService: AuthService,
    private readonly usersService: UsersService,
    private readonly configService: ConfigService,
  ) {
    this.cookieName = this.configService.get<string>('REFRESH_COOKIE');
    this.refreshTime = this.configService.get<number>('jwt.refresh.time');
    this.testing = this.configService.get<string>('NODE_ENV') !== 'production';
  }
}
Enter fullscreen mode Exit fullscreen mode

And add two methods:

  1. Getting refresh token out of the request:

    // ...
    
    @Controller('api/auth')
    @UseGuards(ThrottlerGuard)
    export class AuthController {
      // ...
    
      private refreshTokenFromReq(req: Request): string {
        const token: string | undefined = req.signedCookies[this.cookieName];
    
        if (isUndefined(token)) {
          throw new UnauthorizedException();
        }
    
        return token;
      }
    }
    
  2. Saving the refresh token in a cookie:

    @Controller('api/auth')
    @UseGuards(ThrottlerGuard)
    export class AuthController {
      // ...
    
      private saveRefreshCookie(res: Response, refreshToken: string): Response {
        return res.cookie(this.cookieName, refreshToken, {
          secure: !this.testing,
          httpOnly: true,
          signed: true,
          path: this.cookiePath,
          expires: new Date(Date.now() + this.refreshTime * 1000),
        });
      }
    }
    

Endpoints

The endpoints are the same as the public methods we have on our service.

Sign Up

import {
  Body,
  //...
  Post,
  // ...
} from '@nestjs/common';
// ...
import { Origin } from './decorators/origin.decorator';
import { Public } from './decorators/public.decorator';
// ...
import { SignUpDto } from './dtos/sign-up.dto';


@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Public()
  @Post('/sign-up')
  public async signUp(
    @Origin() origin: string | undefined,
    @Body() signUpDto: SignUpDto,
  ): Promise<IMessage> {
    return this.authService.signUp(signUpDto, origin);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Sign In

import {
  Body,
  HttpStatus,
  Res,
  // ...
} from '@nestjs/common';
import { SignInDto } from './dtos/sign-in.dto';
import { AuthResponseMapper } from './mappers/auth-response.mapper';

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Public()
  @Post('/sign-in')
  public async signIn(
    @Res() res: Response,
    @Origin() origin: string | undefined,
    @Body() singInDto: SignInDto,
  ): Promise<void> {
    const result = await this.authService.signIn(singInDto, origin);
    this.saveRefreshCookie(res, result.refreshToken)
      .status(HttpStatus.OK)
      .json(AuthResponseMapper.map(result));
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Refresh Access

import {
  //...
  Req,
  // ...
} from '@nestjs/common';
// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Public()
  @Post('/refresh-access')
  public async refreshAccess(
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    const token = this.refreshTokenFromReq(req);
    const result = await this.authService.refreshTokenAccess(
      token,
      req.headers.origin,
    );
    this.saveRefreshCookie(res, result.refreshToken)
      .status(HttpStatus.OK)
      .json(AuthResponseMapper.map(result));
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Logout

import {
  //...
  HttpCode,
  // ...
} from '@nestjs/common';
// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Post('/logout')
  @HttpCode(HttpStatus.OK)
  public async logout(
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    const token = this.refreshTokenFromReq(req);
    const message = await this.authService.logout(token);
    res
      .clearCookie(this.cookieName, { path: this.cookiePath })
      .status(HttpStatus.OK)
      .json(message);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Confirm Email

// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Public()
  @Post('/confirm-email')
  public async confirmEmail(
    @Body() confirmEmailDto: ConfirmEmailDto,
    @Res() res: Response,
  ): Promise<void> {
    const result = await this.authService.confirmEmail(confirmEmailDto);
    this.saveRefreshCookie(res, result.refreshToken)
      .status(HttpStatus.OK)
      .json(AuthResponseMapper.map(result));
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Forgot Password

// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Post('/forgot-password')
  @HttpCode(HttpStatus.OK)
  public async forgotPassword(
    @Origin() origin: string | undefined,
    @Body() emailDto: EmailDto,
  ): Promise<IMessage> {
    return this.authService.resetPasswordEmail(emailDto, origin);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Reset Password

// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Public()
  @Post('/reset-password')
  @HttpCode(HttpStatus.OK)
  public async resetPassword(
    @Body() resetPasswordDto: ResetPasswordDto,
  ): Promise<IMessage> {
    return this.authService.resetPassword(resetPasswordDto);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Update Password

import {
  // ...
  Patch,
  // ...
} from '@nestjs/common';
import { CurrentUser } from './decorators/current-user.decorator';
import { ChangePasswordDto } from './dtos/change-password.dto';
// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Patch('/update-password')
  public async updatePassword(
    @CurrentUser() userId: number,
    @Origin() origin: string | undefined,
    @Body() changePasswordDto: ChangePasswordDto,
    @Res() res: Response,
  ): Promise<void> {
    const result = await this.authService.updatePassword(
      userId,
      changePasswordDto,
      origin,
    );
    this.saveRefreshCookie(res, result.refreshToken)
      .status(HttpStatus.OK)
      .json(AuthResponseMapper.map(result));
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Me

This one is necessary to get the current logged user

import {
  // ...
  Get,
  // ...
} from '@nestjs/common';
import { IAuthResponseUser } from '../auth/interfaces/auth-response-user.interface';
import { AuthResponseUserMapper } from '../auth/mappers/auth-response-user.mapper';
// ...

@Controller('api/auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  // ...

  @Get('/me')
  public async getMe(@CurrentUser() id: number): Promise<IAuthResponseUser> {
    const user = await this.usersService.findOneById(id);
    return AuthResponseUserMapper.map(user);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

User Module

User Service

Before adding our controller we need to make some changes to the service:

  1. Add a PasswordDto:

    // password.dto.ts
    
    import { IsString, MinLength } from 'class-validator';
    
    export abstract class PasswordDto {
      @IsString()
      @MinLength(1)
      public password: string;
    }
    
  2. Update the ChangeEmailDto:

    // change-email.dto.ts
    
    import { IsEmail, IsString, Length } from 'class-validator';
    import { PasswordDto } from './password.dto';
    
    export abstract class ChangeEmailDto extends PasswordDto {
      @ApiProperty({
        description: 'The email of the user',
        example: 'someone@gmail.com',
        minLength: 5,
        maxLength: 255,
        type: String,
      })
      @IsString()
      @IsEmail()
      @Length(5, 255)
      public email: string;
    }
    
  3. Add password verification to delete method:

    // ...
    import {
      BadRequestException,
      // ...
    } from '@nestjs/common';
    import { compare, hash } from 'bcrypt';
    // ...
    import { PasswordDto } from './dtos/password.dto';
    import { UserEntity } from './entities/user.entity';
    
    @Injectable()
    export class UsersService {
      // ...
    
      public async delete(userId: number, dto: PasswordDto): Promise<UserEntity> {
        const user = await this.findOneById(userId);
    
        if (!(await compare(dto.password, user.password))) {
          throw new BadRequestException('Wrong password');
        }
    
        await this.commonService.removeEntity(this.usersRepository, user);
        return user;
      }
    
      // ...
    }
    
  4. Add a method to find by id or username:

    // ...
    
    @Injectable()
    export class UsersService {
    // ...
    
      public async findOneByIdOrUsername(
        idOrUsername: string,
      ): Promise<UserEntity> {
        const parsedValue = parseInt(idOrUsername, 10);
    
        if (!isNaN(parsedValue) && parsedValue > 0 && isInt(parsedValue)) {
          return this.findOneById(parsedValue);
        }
    
        if (
          idOrUsername.length < 3 ||
          idOrUsername.length > 106 ||
          !SLUG_REGEX.test(idOrUsername)
        ) {
          throw new BadRequestException('Invalid username');
        }
    
        return this.findOneByUsername(idOrUsername);
      }
    
    // ...
    }
    

User Controller

Mappers

The user return by the user controller should not have the email, so create a new interface:

// response-user.interface.ts

export interface IResponseUser {
  id: number;
  name: string;
  username: string;
  createdAt: string;
  updatedAt: string;
}
Enter fullscreen mode Exit fullscreen mode

And implement it:

// response-user.mapper.ts

import { IResponseUser } from '../interfaces/response-user.interface';
import { IUser } from '../interfaces/user.interface';

export class ResponseUserMapper implements IResponseUser {
  public id: number;
  public name: string;
  public username: string;
  public createdAt: string;
  public updatedAt: string;

  constructor(values: IResponseUser) {
    Object.assign(this, values);
  }

  public static map(user: IUser): ResponseUserMapper {
    return new ResponseUserMapper({
      id: user.id,
      name: user.name,
      username: user.username,
      createdAt: user.createdAt.toISOString(),
      updatedAt: user.updatedAt.toISOString(),
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Endpoints

Start by adding the cookiePath and cookieName for the delete route:

import { Controller } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from './users.service';

@Controller('api/users')
export class UsersController {
  private cookiePath = '/api/auth';
  private cookieName: string;

  constructor(
    private readonly usersService: UsersService,
    private readonly configService: ConfigService,
  ) {
    this.cookieName = this.configService.get<string>('COOKIE_NAME');
  }
}
Enter fullscreen mode Exit fullscreen mode

User controller is comprised mostly by user Read, Update and Delete operations.

Get User

Start by creating a dto for the params:

// get-user.params.ts

import { IsString, Length } from 'class-validator';

export abstract class GetUserParams {
  @IsString()
  @Length(1, 106)
  public idOrUsername: string;
}
Enter fullscreen mode Exit fullscreen mode

And add it as the type of the Param decorator:

import {
  // ...
  Get,
  Param,
  // ...
} from '@nestjs/common';
// ...
import { GetUserParams } from './dtos/get-user.params';
// ...

@Controller('api/users')
export class UsersController {
  // ...

  @Public()
  @Get('/:idOrUsername')
  public async getUser(@Param() params: GetUserParams): Promise<IResponseUser> {
    const user = await this.usersService.findOneByIdOrUsername(idOrUsername);
    return ResponseUserMapper.map(user);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Update Email

Since it updtes the email we need to return AuthResponseUserMapper:

import {
  Body,
  // ...
  Patch,
} from '@nestjs/common';
// ...
import { ChangeEmailDto } from './dtos/change-email.dto';
// ...

@Controller('api/users')
export class UsersController {
  // ...

  @Patch('/email')
  public async updateEmail(
    @CurrentUser() id: number,
    @Body() dto: ChangeEmailDto,
  ): Promise<IAuthResponseUser> {
    const user = await this.usersService.updateEmail(id, dto);
    return AuthResponseUserMapper.map(user);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Update User

// ...
import { UpdateUserDto } from './dtos/update-user.dto';
// ...

@Controller('api/users')
export class UsersController {
  // ...

  @Patch()
  public async updateUser(
    @CurrentUser() id: number,
    @Body() dto: UpdateUserDto,
  ): Promise<IResponseUser> {
    const user = await this.usersService.update(id, dto);
    return ResponseUserMapper.map(user);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Delete User

import {
  // ...
  Delete,
  // ...,
  HttpStatus,
  // ...
} from '@nestjs/common';
import { PasswordDto } from './dtos/password.dto';
// ...

@Controller('api/users')
export class UsersController {
  // ...

  @Delete()
  @ApiNoContentResponse({
    description: 'The user is deleted.',
  })
  @ApiBadRequestResponse({
    description: 'Something is invalid on the request body, or wrong password.',
  })
  @ApiUnauthorizedResponse({
    description: 'The user is not logged in.',
  })
  public async deleteUser(
    @CurrentUser() id: number,
    @Body() dto: PasswordDto,
    @Res() res: Response,
  ): Promise<void> {
    await this.usersService.delete(id, dto);
    res
      .clearCookie(this.cookieName, { path: this.cookiePath })
      .status(HttpStatus.NO_CONTENT)
      .send();
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Main file

Finally we need to set-up our main file, start by installing cookie-parser and helmet:

$ yarn add cookie-parser helmet
$ yarn add -D @types/cookie-parser
Enter fullscreen mode Exit fullscreen mode

And add them, as well as a validation pipe to the main file:

import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  app.use(cookieParser(configService.get<string>('COOKIE_SECRET')));
  app.use(helmet());
  app.enableCors({
    credentials: true,
    origin: `https://${configService.get<string>('domain')}`,
  });
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
    }),
  );
  await app.listen(configService.get<number>('port'));
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

(Optional) Swagger

How will our front-end team know how to use our API? This is where documentation is important, to create documentation automatic we can use NestJS Swagger, start by installing it:

$ yarn add @nestjs/swagger
Enter fullscreen mode Exit fullscreen mode

And add descriptions to all DTOS, Mappers and endpoints, I will exemplify with the users, but is exactly the same process for the auth.

DTOs

Add ApiProperty decorators all dtos:

Username DTO

import { ApiProperty } from '@nestjs/swagger';
import { IsString, Length, Matches } from 'class-validator';
import { SLUG_REGEX } from '../../common/consts/regex.const';

export abstract class UsernameDto {
  @ApiProperty({
    description: 'The username of the user',
    minLength: 3,
    maxLength: 106,
    example: 'my-username',
    type: String,
  })
  @IsString()
  @Length(3, 106)
  @Matches(SLUG_REGEX, {
    message: 'Username must be a valid slugs',
  })
  public username: string;
}
Enter fullscreen mode Exit fullscreen mode

Password DTO

import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';

export abstract class PasswordDto {
  @ApiProperty({
    description: 'The password of the user',
    minLength: 1,
    type: String,
  })
  @IsString()
  @MinLength(1)
  public password: string;
}
Enter fullscreen mode Exit fullscreen mode

Change Email DTO

import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString, Length } from 'class-validator';
import { PasswordDto } from './password.dto';

export abstract class ChangeEmailDto extends PasswordDto {
  @ApiProperty({
    description: 'The email of the user',
    example: 'someone@gmail.com',
    minLength: 5,
    maxLength: 255,
    type: String,
  })
  @IsString()
  @IsEmail()
  @Length(5, 255)
  public email: string;
}
Enter fullscreen mode Exit fullscreen mode

Get User Params*

import { ApiProperty } from '@nestjs/swagger';
import { IsString, Length } from 'class-validator';

export abstract class GetUserParams {
  @ApiProperty({
    description: 'The id or username of the user',
    type: String,
    example: "1 or 'username'",
  })
  @IsString()
  @Length(1, 106)
  public idOrUsername: string;
}
Enter fullscreen mode Exit fullscreen mode

Mappers

Add ApiProperty decorators the ResponseUserMapper:

import { ApiProperty } from '@nestjs/swagger';
import { IResponseUser } from '../interfaces/response-user.interface';
import { IUser } from '../interfaces/user.interface';

export class ResponseUserMapper implements IResponseUser {
  @ApiProperty({
    description: 'User id',
    example: 123,
    minimum: 1,
    type: Number,
  })
  public id: number;

  @ApiProperty({
    description: 'User name',
    example: 'John Doe',
    minLength: 3,
    maxLength: 100,
    type: String,
  })
  public name: string;

  @ApiProperty({
    description: 'User username',
    example: 'john.doe1',
    minLength: 3,
    maxLength: 106,
    type: String,
  })
  public username: string;

  @ApiProperty({
    description: 'User creation date',
    example: '2021-01-01T00:00:00.000Z',
    type: String,
  })
  public createdAt: string;

  @ApiProperty({
    description: 'User last update date',
    example: '2021-01-01T00:00:00.000Z',
    type: String,
  })
  public updatedAt: string;

  constructor(values: IResponseUser) {
    Object.assign(this, values);
  }

  public static map(user: IUser): ResponseUserMapper {
    return new ResponseUserMapper({
      id: user.id,
      name: user.name,
      username: user.username,
      createdAt: user.createdAt.toISOString(),
      updatedAt: user.updatedAt.toISOString(),
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Enpoints

NestJS swagger exports an api response type for each HTTP status code, add one for each response an endpoint can have, with a type of a Mapper for the 200's status codes:

// ...
import {
  ApiBadRequestResponse,
  ApiNoContentResponse,
  ApiNotFoundResponse,
  ApiOkResponse,
  ApiTags,
  ApiUnauthorizedResponse,
} from '@nestjs/swagger';
// ...

@ApiTags('Users')
@Controller('api/users')
export class UsersController {
  // ...

  @Public()
  @Get('/:idOrUsername')
  @ApiOkResponse({
    type: ResponseUserMapper,
    description: 'The user is found and returned.',
  })
  @ApiBadRequestResponse({
    description: 'Something is invalid on the request body',
  })
  @ApiNotFoundResponse({
    description: 'The user is not found.',
  })
  public async getUser(
    // ...
  ): Promise<IResponseUser> {
    // ...
  }

  @Patch('/email')
  @ApiOkResponse({
    type: AuthResponseUserMapper,
    description: 'The email is updated, and the user is returned.',
  })
  @ApiBadRequestResponse({
    description: 'Something is invalid on the request body, or wrong password.',
  })
  @ApiUnauthorizedResponse({
    description: 'The user is not logged in.',
  })
  public async updateEmail(
    // ...
  ): Promise<IAuthResponseUser> {
    // ...
  }

  @Patch()
  @ApiOkResponse({
    type: ResponseUserMapper,
    description: 'The username is updated.',
  })
  @ApiBadRequestResponse({
    description: 'Something is invalid on the request body.',
  })
  @ApiUnauthorizedResponse({
    description: 'The user is not logged in.',
  })
  public async updateUser(
    // ...
  ): Promise<IResponseUser> {
    // ...
  }

  @Delete()
  @ApiNoContentResponse({
    description: 'The user is deleted.',
  })
  @ApiBadRequestResponse({
    description: 'Something is invalid on the request body, or wrong password.',
  })
  @ApiUnauthorizedResponse({
    description: 'The user is not logged in.',
  })
  public async deleteUser(
    // ...
  ): Promise<void> {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Main file

Finally, add the document builder to your main file:

// ...
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
// ...

async function bootstrap() {
  // ...

  const swaggerConfig = new DocumentBuilder()
    .setTitle('NestJS Authentication API')
    .setDescription('A OAuth2.0 authentication API made with NestJS')
    .setVersion('0.0.1')
    .addBearerAuth()
    .addTag('Authentication API')
    .build();
  const document = SwaggerModule.createDocument(app, swaggerConfig);
  SwaggerModule.setup('api/docs', app, document);

  // ...
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

Conclusion

With this you now have a full functioning Authentication Microsservice to use on your REST apps. The github for this tutorial can be found here.

About the Author

Hey there! I am Afonso Barracha, your go-to econometrician who found his way into the world of back-end development with a soft spot for GraphQL. If you enjoyed reading this article, why not show some love by buying me a coffee?

Lately, I have been diving deep into more advanced subjects. As a result, I have switched from sharing my thoughts every week to posting once or twice a month. This way, I can make sure to bring you the highest quality content possible.

Do not miss out on any of my latest articles – follow me here on dev and LinkedIn to stay updated. I would be thrilled to welcome you to our ever-growing community! See you around!

Top comments (6)

Collapse
 
mubasharstack profile image
Mubashar Ahmed

Nice article πŸ‘πŸ‘ Currently using this on my project.kindly post the rest of the parts as soon as possible.i'm new to nestjs and this is my first time with nestjs.
i just created my account on dev.to just to thanks you.
Thank you so much for this series

Collapse
 
tugascript profile image
Afonso Barracha

Thanks, I will try to update this series once a week, just follow me to get the next update between Friday and Sunday next week.

Note that after I finish this series, the next series will be on the Tokio stack and Rust for TypeScript NodeJS Developer, so it may not interest you.

However, on May or June I will create a series on Advance NestJS so keep tuned for that. It will cover the development of a NestJS Hybrid Monolith API (REST & GraphQL) and transforming it into a microservices architecture.

Collapse
 
antoinemaitre profile image
Antoine Maitre

This looks nice ! Great effort on the detailed imports sections as well as using Swagger documentation in a proper way πŸ‘ πŸ’― πŸš€ I've been implementing such endpoints for job purposes and your code has similarities, so it's good to see !
Also, I'll check the authentication course from NestJS official team and see how I'll implement my next project's authentication and users management. Might maybe use an external provider to handle the job and remove the additional dev to do these tasks πŸ€” Will see ! Anyways, great job again ! ;)

Collapse
 
dominusprime profile image
Dominus Prime

Thank you so much for this. I know passport is not absolutely necessary, however I'd like to understand your reasoning for not using it.

Collapse
 
tugascript profile image
Afonso Barracha

I mainly do not use Passport since I prefer Fastify, as unlike Express, it is actively maintained. So I got used to not have access to it.

That being said, since guards already exist in NestJS I do not see any need for an extra layer of abstraction.

Collapse
 
tugascript profile image
Afonso Barracha

So I added some site for testing and forgot to remove on the article, the same site strict cookie policy should not be used in production.