인가 처리를 하기 위해 일반적으로 많이 쓰이는 방법은 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-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')
}
}
}
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()
는 그 외에 내가 커스텀할 게 없기 때문에 원래 구현된 대로 동작하게끔 유도하는 구문이다.
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
info 로 변경되어야 합니다.