[Nest.js] Authguard 구현 시 다양한 에러 핸들링 적용하기

Seung Hyeon ·2023년 12월 16일
0

백엔드

목록 보기
10/19
post-thumbnail

기존의 Authguard 구현 방법

인가 처리를 하기 위해 일반적으로 많이 쓰이는 방법은 passport-jwt의 Strategy를 전략으로 넣어 PassportStrategy를 확장하는 방법이다.

jwt.strategy.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserEntity } from './entities/user.entity';
import { Repository } from 'typeorm';

@Injectable()  // JwtStrategy를 다른 곳에서도 사용할 수 있도록
export class JwtStrategy extends PassportStrategy(Strategy) {  
  constructor(
    @InjectRepository(UserEntity)
    private usersRepository: Repository<UserEntity>,
  ) {
    super({  // 부모 컴포넌트를 사용하기 위해 super사용
      secretOrKey: process.env.JWT_SECRET_KEY, // 토큰 생성했던 것과 같은 키, 토큰이 유효한지 확인
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 클라이언트에서 오는 토큰이 어디서 오는지(AuthHeader, BearerToken 타입으로 온다)
      ignoreExpiration: false,
    });
  }
	
	// 토큰이 유효하다고 생각하면 수행되는 메소드 (@UseGuards시 사용될 메소드)
  async validate(payload) {
    console.log(payload);  // 💚
  }

💚 출력 결과

{
  userId: '235b91a9-659d-4a26-abfd-********7',
  iat: 1701147626,
  exp: 1701151226
}

passport-jwt에서 제공하는 Strategy는 super() 연산자로 토큰 유효성 확인과 헤더에서의 토큰 추출을 한번에 할 수 있어서 바로 payload을 도출해낼 수 있어 편리하다.

그러나 JWT 인가 에러가 하나같이 모두 401에러로 통일되어서 나오고 상황에 맞는 에러 메시지를 띄우기 힘들다는 단점이 있다.
(try catch로 예러 핸들링을 시도했지만 잘 되지 않았다.)

토큰 값이 유효하지 않는 에러, 토큰 값이 비어있는 에러 등을 다르게 구분하고 싶었고, 상황에 맞는 에러 메시지도 함꼐 띄우고 싶었다.

Passport-Custom의 Strategy를 이용하자

passport-jwt라이브러리의 Strategy를 사용하는 대신, passport-custom 라이브러리의 Strategy를 이용하면 에러를 커스텀 할 수 있다.

# npm을 사용하는 경우
npm install passport-custom

# yarn을 사용하는 경우
yarn add passport-custom

나중에 구현할 refresh 인증 전략을 구분하기 위해, access 토큰 관련 인증 전략의 이름을 'acccess'로 정의했다.

다음 상황에서 적절한 에러처리 구현

  • 헤더에 access token이 없는 경우 → BadRequestException (400 에러)
  • JWT 토큰이 만료된 경우 → UnauthorizedException (401 에러)
  • 토큰 값이 잘못된 경우 → BadRequestException (400에러)
    SyntaxError : 토큰이 유효하지 않는 JSON형식일 경우 (jwt.vertify 에서 발생)
    JsonWebTokenError : JWT의 유효성 겁사에 실패했을 때
import { BadRequestException, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-custom';
import * as jwt from 'jsonwebtoken';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'access') {
  constructor(private configService: ConfigService) {
    super();
  }
  async validate(req: Request) {
    try {
      const userToken = req.headers['authorization']?.slice(7);

      if (!userToken) {
        throw new BadRequestException('There is no access token in header');
      }

      const secretKey = this.configService.get<string>('JWT_SECRET_KEY');
      const payload = jwt.verify(userToken, secretKey);
      const userId = payload['userId'];
      return userId;
    } catch (e) {
      console.error(e);
      if (e instanceof BadRequestException) {
        throw e
      }
      if (e instanceof SyntaxError) {
        throw new BadRequestException('Invalid JSON object');
      }
      if (e instanceof TokenExpiredError) {
        throw new UnauthorizedException('Access Token is expired');
      }
      if (e instanceof JsonWebTokenError) {
        throw new BadRequestException(e.message);
      }
      else {
        throw new UnauthorizedException('Unauthorized for unknown error')
    }
  }
}

의미의 명료화와 코드 간소화를 위해 JwtAuthGuard 클래스 도입

Controller에서 AuthGuard를 사용할 때, 의미를 더 분명히 하고 JwtStrategy의 코드량도 줄이기 위해 JwtAuthGuard 클래스를 도입했다.

validate메서드의 catch문 간소화를 위해 JwtAuthGuard를 따로 만들어준다.
JwtAuthGuard은 그저 AuthGuard를 확장하는 인터페이스 역할을 한다.

JwtStrategy 클래스에서 validate 를 통해서 검증된 사용자의 정보가 handleRequest 메서드에서 에러 분기처리되게 하였다. (즉 validate 메소의 catch문의 에러 분기처리를 handleRequest로 옮김)

jwt-auth.guard.ts

import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { TokenExpiredError, JsonWebTokenError } from 'jsonwebtoken';

@Injectable()
export class JwtAuthGuard extends AuthGuard('access') {
  handleRequest(err: any, user: any, info: any, context: any, status: any) {
    if (err || !user) {
      if (err instanceof BadRequestException) {
        throw err;
      }
      if (err instanceof TokenExpiredError) {
        throw new UnauthorizedException('Access Token is expired');
      }
      if (err instanceof JsonWebTokenError) {
        throw new BadRequestException(err.message);
      }
      if (err instanceof SyntaxError) {
        throw new BadRequestException('Invalid JSON object');
      } else {
        throw new UnauthorizedException('Unauthorized');
      }
    }
    return super.handleRequest(err, user, info, context, status);
  }
}

return의 super.handleRequest()는 그 외에 내가 커스텀할 게 없기 때문에 원래 구현된 대로 동작하게끔 유도하는 구문이다.

JwtAuthGuard 사용 예시

JWT 토큰을 사용하는 곳에 @UseGuards(JwtAuthGuard) 를 붙여서 사용한다.
user.controller.ts

@UseGuards(JwtAuthGuard)
@Get('/me')
async getAuthUserInfo(@Request() req): Promise<{ message: string; result: any }> {
  const result = await this.usersService.getAuthUserInfo(req.user);
return { message: '당신의 정보를 성공적으로 가져왔습니다.', result };
}

 


※ 참고
https://v3.leedo.me/b3a73fa7-5dc5-44a7-99e7-bc8a715e42b9

profile
안되어도 될 때까지

1개의 댓글

comment-user-thumbnail
2024년 9월 5일
  if (info instanceof TokenExpiredError) {
    throw new UnauthorizedException('Access Token is expired');
  }
  if (info instanceof JsonWebTokenError) {
    throw new BadRequestException(err.message);
  }

info 로 변경되어야 합니다.

답글 달기