개발
home
⛴️

Passport Custom Strategy for JWT + NestJS

2021-12-14 @이영훈
Passport의 JWT를 그대로 이용하기에는 상황에 맞는 에러 메시지를 띄우기 힘들었습니다. 그래서 Passport의 Custom Strategy를 이용하여 JWT의 Access Token 인증을 구현하였습니다

Install Package

passport-custom을 설치합니다
# npm을 사용하는 경우 npm install passport-custom # yarn을 사용하는 경우 yarn add passport-custom
Bash
복사
그리고 jsonwebtoken도 설치합니다
# npm을 사용하는 경우 npm install jsonwebtoken npm install --save @types/jsonwebtoken # yarn을 사용하는 경우 yarn add jsonwebtoken yarn add --dev @types/jsonwebtoken
Bash
복사

jwt.strategy.ts 파일 작성

passport-custom과 jsonwebtoken을 이용하여 구현합니다.
HTTP header에 Bearer Authorization 방식으로 JWT access token을 받는 경우를 구현했습니다.
JWT 토큰에는 id, nickname, email, imgPath(썸네일) 등의 기본 정보값이 저장되어 있습니다.
그리고 다음 상황에서 적절한 에러처리를 했습니다
1.
헤더에 access token이 없는 경우 → BadRequestException (400 에러)
2.
JWT가 access token이 아닌 경우 → JwtTypeError (400 에러)
3.
JWT payload가 잘 못 된 경우 (base64 decode 에러가 나는 경우 등) → BadRequestException (400 에러)
4.
JWT 토큰이 만료된 경우 → UnauthorizedException (401 에러) [403보다 401이 올바릅니다]
5.
signature가 올바르지 않거나 header가 올바르지 않은 경우 → BadRequestException (400 에러)
// jwt.strategy.ts import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-custom'; import { jwtConstants } from './constants'; import * as jwt from 'jsonwebtoken'; import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; import { BasicUserDto } from '../users/dto/basic-user.dto'; import { TokenType } from './enums'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { async validate(req: Request) { const token = req.headers['authorization']?.slice(7); if (!token) { throw new BadRequestException('There is no access token in header'); } try { jwt.verify(token, jwtConstants.secret); const payload = jwt.decode(token); if (payload['type'] !== TokenType.ACCESS_TOKEN) { throw new JwtTypeError('Token is not access token'); } return new BasicUserDto(payload['id'], payload['nickname'], payload['email'], payload['imgPath']); } catch (e) { if (e instanceof SyntaxError) { // payload가 잘 못 되었을 때 (base64 decode가 안되는 경우 등) throw new BadRequestException('Invalid JSON object'); } if (e instanceof TokenExpiredError) { throw new UnauthorizedException('Token is expired'); } if (e instanceof JsonWebTokenError) { // JwtWebTokenError should be later than TokenExpiredError // invalid signature | invalid token (header 깨졌을 때) throw new BadRequestException(e.message); } if (e instanceof JwtTypeError) { throw new BadRequestException('Token is not access token'); } throw e; } } } class JwtTypeError extends Error { constructor(message: string) { super(message); } }
TypeScript
복사
// basic-user.dto.ts export class BasicUserDto { readonly id: number; readonly nickname: string; readonly email: string; readonly imgPath: string; constructor(id: number, nickname: string, email: string, imgPath: string) { this.id = id; this.nickname = nickname; this.email = email; this.imgPath = imgPath; } }
TypeScript
복사

jwt-auth.guard.ts 파일 작성

Guard를 사용할 때 의미를 더 분명히하고 코드량도 줄이기 위해 JwtAuthGuard 클래스를 만들었습니다 (참고)
// jwt-auth.guard.ts import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {}
TypeScript
복사

사용 예제

JWT 토큰을 사용하는 곳에 @UseGuards(JwtAuthGuard) 를 붙여서 사용합니다
// user.controller.ts @ApiBearerAuth('access-token') @ApiOperation({ summary: '사용자 정보 API', description: '사용자의 정보를 가져옵니다' }) @ApiOkResponse({ type: UserInfoDto }) @UseGuards(JwtAuthGuard) @Header('Cache-Control', 'no-cache') @Get('/users/me') async findUserInfo(@Request() req) { const { id: userId } = req.user as BasicUserDto; return this.userService.getUserInfo(userId); }
TypeScript
복사