Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 27, 2023 02:29 pm GMT

NestJS Authentication with OAuth2.0: Fastify Local OAuth

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 third part of this series.

Tutorial Intro

On this tutorial we will change the adapter from our previous REST API, from Express to Fastify.

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

Set Up

Start by removing express and its dependencies:

$ yarn remove @types/express @types/express-serve-static-core @nestjs/platform-express cookie-parser helmet @types/cookie-parser

And install the fastify ones:

$ yarn add @nestjs/platform-fastify fastify @fastify/cookie @fastify/cors @fastify/csrf-protection @fastify/helmet

Auth Module

Guards

We need to remove express.d.ts and add fastify.d.ts:

import { FastifyRequest as Request } from 'fastify';declare module 'fastify' {  interface FastifyRequest extends Request {    user?: number;  }}

Auth Guard

Only one of the types change, from Request to FastifyRequest:

import {  CanActivate,  ExecutionContext,  Injectable,  UnauthorizedException,} from '@nestjs/common';import { Reflector } from '@nestjs/core';import { isJWT } from 'class-validator';import { FastifyRequest } from 'fastify';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<FastifyRequest>(),      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: FastifyRequest,    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;    }  }}

(Optional) Throttler Guard

We can no longer use the NestJS default ThrottlerGuard as that one is implemented for express, so create a custom one:

// fastify-throttler.guard.tsimport { ExecutionContext, Injectable } from '@nestjs/common';import { ThrottlerGuard } from '@nestjs/throttler';import { FastifyReply, FastifyRequest } from 'fastify';@Injectable()export class FastifyThrottlerGuard extends ThrottlerGuard {  public getRequestResponse(context: ExecutionContext) {    const http = context.switchToHttp();    return {      req: http.getRequest<FastifyRequest>(),      res: http.getResponse<FastifyReply>(),    };  }}

And add it to the auth controller:

// ...import { FastifyThrottlerGuard } from './guards/fastify-throttler.guard';// ...@ApiTags('Auth')@Controller('api/auth')@UseGuards(FastifyThrottlerGuard)export class AuthController {  // ...}

Controller

Private Methods

There is a slight change on the refresTokenFromReq method:

import {  // ...  Controller,  // ...  UseGuards,} from '@nestjs/common';import { ConfigService } from '@nestjs/config';import {  // ...  ApiTags,  // ...} from '@nestjs/swagger';import { FastifyReply, FastifyRequest } from 'fastify';// ...import { isNull, isUndefined } from '../common/utils/validation.util';// ...@ApiTags('Auth')@Controller('api/auth')@UseGuards(FastifyThrottlerGuard)export class AuthController {  // ...  private refreshTokenFromReq(req: FastifyRequest): string {    const token: string | undefined = req.cookies[this.cookieName];    if (isUndefined(token) || isNull(token)) {      throw new UnauthorizedException();    }    const { valid, value } = req.unsignCookie(token);    if (!valid) {      throw new UnauthorizedException();    }    return value;  }  // ...}

And since fastify does not have the json method we need to set the Content-Type header to application/json:

// ...@ApiTags('Auth')@Controller('api/auth')@UseGuards(FastifyThrottlerGuard)export class AuthController {  // ...  private saveRefreshCookie(    res: FastifyReply,    refreshToken: string,  ): FastifyReply {    return res      .cookie(this.cookieName, refreshToken, {        secure: !this.testing,        httpOnly: true,        signed: true,        path: this.cookiePath,        expires: new Date(Date.now() + this.refreshTime * 1000),      })      .header('Content-Type', 'application/json');  }}

Enpoints

There aren't that many changes that we need to implement on our previous express API, we just need to change from:

  • Request to FastifyRequest on the Req decorator;
  • Response to FastifyReply on the Res decorator;
  • json to send method.
// ...@ApiTags('Auth')@Controller('api/auth')@UseGuards(FastifyThrottlerGuard)export class AuthController {  // ...  @Public()  @Post('/sign-up')  @ApiCreatedResponse({    type: MessageMapper,    description: 'The user has been created and is waiting confirmation',  })  @ApiConflictResponse({    description: 'Email already in use',  })  @ApiBadRequestResponse({    description: 'Something is invalid on the request body',  })  public async signUp(    @Origin() origin: string | undefined,    @Body() signUpDto: SignUpDto,  ): Promise<IMessage> {    return await this.authService.signUp(signUpDto, origin);  }  @Public()  @Post('/sign-in')  @ApiOkResponse({    type: AuthResponseMapper,    description: 'Logs in the user and returns the access token',  })  @ApiBadRequestResponse({    description: 'Something is invalid on the request body',  })  @ApiUnauthorizedResponse({    description: 'Invalid credentials or User is not confirmed',  })  public async signIn(    @Res() res: FastifyReply,    @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)      .send(AuthResponseMapper.map(result));  }  @Public()  @Post('/refresh-access')  @ApiOkResponse({    type: AuthResponseMapper,    description: 'Refreshes and returns the access token',  })  @ApiUnauthorizedResponse({    description: 'Invalid token',  })  @ApiBadRequestResponse({    description:      'Something is invalid on the request body, or Token is invalid or expired',  })  public async refreshAccess(    @Req() req: FastifyRequest,    @Res() res: FastifyReply,  ): 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)      .send(AuthResponseMapper.map(result));  }  @Post('/logout')  @ApiOkResponse({    type: MessageMapper,    description: 'The user is logged out',  })  @ApiBadRequestResponse({    description: 'Something is invalid on the request body',  })  @ApiUnauthorizedResponse({    description: 'Invalid token',  })  public async logout(    @Req() req: FastifyRequest,    @Res() res: FastifyReply,  ): Promise<void> {    const token = this.refreshTokenFromReq(req);    const message = await this.authService.logout(token);    res      .clearCookie(this.cookieName, { path: this.cookiePath })      .header('Content-Type', 'application/json')      .status(HttpStatus.OK)      .send(message);  }  @Public()  @Post('/confirm-email')  @ApiOkResponse({    type: AuthResponseMapper,    description: 'Confirms the user email and returns the access token',  })  @ApiUnauthorizedResponse({    description: 'Invalid token',  })  @ApiBadRequestResponse({    description:      'Something is invalid on the request body, or Token is invalid or expired',  })  public async confirmEmail(    @Origin() origin: string | undefined,    @Body() confirmEmailDto: ConfirmEmailDto,    @Res() res: FastifyReply,  ): Promise<void> {    const result = await this.authService.confirmEmail(confirmEmailDto);    this.saveRefreshCookie(res, result.refreshToken)      .status(HttpStatus.OK)      .send(AuthResponseMapper.map(result));  }  @Public()  @Post('/forgot-password')  @HttpCode(HttpStatus.OK)  @ApiOkResponse({    type: MessageMapper,    description:      'An email has been sent to the user with the reset password link',  })  public async forgotPassword(    @Origin() origin: string | undefined,    @Body() emailDto: EmailDto,  ): Promise<IMessage> {    return this.authService.resetPasswordEmail(emailDto, origin);  }  @Public()  @Post('/reset-password')  @HttpCode(HttpStatus.OK)  @ApiOkResponse({    type: MessageMapper,    description: 'The password has been reset',  })  @ApiBadRequestResponse({    description:      'Something is invalid on the request body, or Token is invalid or expired',  })  public async resetPassword(    @Body() resetPasswordDto: ResetPasswordDto,  ): Promise<IMessage> {    return this.authService.resetPassword(resetPasswordDto);  }  @Patch('/update-password')  @ApiOkResponse({    type: AuthResponseMapper,    description: 'The password has been updated',  })  @ApiUnauthorizedResponse({    description: 'The user is not logged in.',  })  public async updatePassword(    @CurrentUser() userId: number,    @Origin() origin: string | undefined,    @Body() changePasswordDto: ChangePasswordDto,    @Res() res: FastifyReply,  ): Promise<void> {    const result = await this.authService.updatePassword(      userId,      changePasswordDto,      origin,    );    this.saveRefreshCookie(res, result.refreshToken)      .status(HttpStatus.OK)      .send(AuthResponseMapper.map(result));  }  @Get('/me')  @ApiOkResponse({    type: AuthResponseUserMapper,    description: 'The user is found and returned.',  })  @ApiUnauthorizedResponse({    description: 'The user is not logged in.',  })  public async getMe(@CurrentUser() id: number): Promise<IAuthResponseUser> {    const user = await this.usersService.findOneById(id);    return AuthResponseUserMapper.map(user);  }  // ...}

User Module

Controller

There is just one change on the return type of the Res decorator (from Response to FastifyReply) on the delete endpoint:

// ...import { FastifyReply } from 'fastify';// ...@ApiTags('Users')@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: FastifyReply,  ): Promise<void> {    await this.usersService.delete(id, dto);    res      .clearCookie(this.cookieName, { path: this.cookiePath })      .status(HttpStatus.NO_CONTENT)      .send();  }}

Main

Finally change the main file to a NestFastifyApplication, and register all the plugins we installed earlier:

import fastifyCookie from '@fastify/cookie';import fastifyCors from '@fastify/cors';import fastifyCsrfProtection from '@fastify/csrf-protection';import fastifyHelmet from '@fastify/helmet';import { ValidationPipe } from '@nestjs/common';import { ConfigService } from '@nestjs/config';import { NestFactory } from '@nestjs/core';import {  FastifyAdapter,  NestFastifyApplication,} from '@nestjs/platform-fastify';import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';import { AppModule } from './app.module';async function bootstrap() {  const app = await NestFactory.create<NestFastifyApplication>(    AppModule,    new FastifyAdapter(),  );  const configService = app.get(ConfigService);  app.register(fastifyCookie, {    secret: configService.get<string>('COOKIE_SECRET'),  });  app.register(fastifyHelmet);  app.register(fastifyCsrfProtection, { cookieOpts: { signed: true } });  app.register(fastifyCors, {    credentials: true,    origin: `https://${configService.get<string>('domain')}`,  });  app.useGlobalPipes(    new ValidationPipe({      transform: true,    }),  );  const swaggerConfig = new DocumentBuilder()    .setTitle('NestJS Authentication API')    .setDescription('An 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);  await app.listen(    configService.get<number>('port'),    configService.get<boolean>('testing') ? '127.0.0.1' : '0.0.0.0',  );}bootstrap();

Conclusion

I hope that I have shown just how easy it is to change an API from Express to Fastify with NestJS.

The github for this tutorial can be found here.

About the Author

Hey there my name is Afonso Barracha, I am a Econometrician made back-end developer that has a passion for GraphQL.

I used to try to post once a week, but I am diving into more advance topics which take more time to write, but I still try to post at least twice a month.

If you do not want to lose any of my posts follow me here on dev, or on LinkedIn.


Original Link: https://dev.to/tugascript/nestjs-authentication-with-oauth20-fastify-local-oauth-5gn9

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To