NestJS에서 토큰인증

권태형·2023년 4월 24일
0

NestJS 연습

목록 보기
10/19
post-thumbnail

이전 시간에는 NestJS에서 JWT토큰을 발급하는 코드에 대해서 설명하였다. 토큰은 사용자 인증을 위해서 발급하는 것이기 때문에 당연히 인증을 위한 로직이 필요하게 된다. 이번 포스팅에서는 NestJS에서 Jwt토큰을 인증하고 인증된 사용자의 정보를 어떻게 얻을 수 있는지 알아보자.

express에서 jwt토큰을 인증하면 라우터단위의 미들웨어에서 인증하여 res.locals에서 user객체를 뽑아 안에 담아두었던 필요한 데이터를 꺼내서 사용했었다.

NestJs에서도 마찬가지로 라우터단위의 미들웨어를 사용해서 인증하는데 저번에 유효성검사 포스팅에서 사용한 pipe와 달리 사용자인증에서 사용되는 미들웨어는 Guard로 따로 분류한다.

NestJS에서는 기능이나 사용용도에 맞춰서 미들웨어를 각각 분류해 놓았다. Pipe, Guard, Filter, Interceptor등이 있다.

NestJS의 공식페이지에서는 Guard의 로직자체를 작성하여 JWT를 인증하는 방식을 설명해준다.

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';

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

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(
        token,
        {
          secret: jwtConstants.secret
        }
      );
      // 💡 We're assigning the payload to the request object here
      // so that we can access it in our route handlers
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

// --------------------------------------
//controller

 @UseGuards(AuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }

위 방식은 우리가 express에서 인증 미들웨어를 만들어 사용하는 방식과 매우 비슷하다. 단지 미들웨어를 클래스 AuthGuard로 정했을 뿐, req의 헤더에서 토큰을 뽑아서 토큰에 대한 검증을 실시한다. /auth/profile 경로의 라우터단위에서 @UseGuards데코레이터를 사용해서 검증하고 검증에 성공하면 req.user에 토큰에서 검증한 payload값이 담기게 된다.

NestJS에서 위의 방법보다 더 간단하게 Guard를 이용해 인증을 처리하는 방식이 있다. passport를 이용한 Guard - Strategy 구조로 인증을 처리하는 방식이다.

npm i --save passport passport-jwt @nestjs/passport @types/passport-jwt

passport에 필요한 관련 라이브러리들을 설치해주고, Guard와 stratey를 구연해주면 위의 코드와 같이 UseGuards를 이용해 커스텀가드로 토큰을 검증할 수 있다.

위의 가드 로직과 달리, passport를 이용할 때 Guard는 로직이 없다고 생각해도 무방할 정도로 아무내용이 없다. 단지 상속받는 AuthGuard()에 어떤한 파라미터를 줄 것인지만 생각하면 된다. Guard는 이 파라미터로 받는 name(string)으로 필요한 Strategy를 내부로직을 통해 찾아가서 실행한다.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class AccessTokenGuard extends AuthGuard('access') {}

위는 가드의 로직이다. 정말 아무것도 없고 extends AuthGuard('access')만 존재한다. 'access'는 내가 직접 커스텀 Strategy를 'access'로 만들었기 때문에 'access'라는 네이밍을 사용하였지만, 보통 'jwt'를 기본값으로 사용한다.

그렇다면 검증하는 로직은 당연히 strategy파일에 있을 것이다.

//access.strategy.ts
import { AuthService } from '../auth.service';
import { Payload } from './jwt.payload.interface';

@Injectable()
export class AccessStrategy extends PassportStrategy(Strategy, 'access') {
  constructor(private authService: AuthService) {
    super({
      //AuthHeader의 엑세스토큰을 검증
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      //만료시간을 무시할지 말지(ture: 만료되어도 검증 성공, false: 만료된다면 검증 실패)
      ignoreExpiration: false,
      //엑세스토큰 검증 비밀키
      secretOrKey: process.env.ACCESS_JWT_SECRET,
    });
  }

  async validate(payload: Payload, done: VerifiedCallback): Promise<any> {
    const user = await this.authService.tokenValidateUser(payload);
    if (!user) {
      return done(
        new UnauthorizedException({ message: '존재 하지 않는 유저' }),
        false,
      );
    }
    return done(null, user);
  }
}
//-------------------------------
//auth.service.ts
async tokenValidateUser(payload: Payload): Promise<User | undefined> {
    return await this.userService.findById(payload.id);
  }
//-------------------------------
//user.service.ts
 async findById(id: number): Promise<User | undefined> {
    return await this.userRepository.findOne({
      where: { id },
      select: ['id', 'nickname'],
    });
  }

주석이 많아서 그렇지 실질적으로는 이 코드가 좀 더 간단해 보이고 작성하기 더 쉽다. 솔찍히 검증하는 구간은 super({})구간에서 검증은 완료한다. 아래의 validate함수는 검증을 완료한 payload에서 DB와 비교해 해당 유저가 있는지 확인하고 req에 user데이터를 담기위해 필요한 로직이다.

일일히 헤더에서 꺼내서 Bearer를 나누고 토큰을 꺼내서 또 다시 검증할 필요가 없다. 그저 어디서, 어떤옵션을 가지고, 어떤 비밀키로 검증할 것인지만 작성하면 간단하게 검증할 수 있다.

fromAuthHeaderAsBearerToken()함수는 AuthHeader에 있는 Bearer토큰을 검증하는 함수이다. 일반적으로 Header에 accessToken을 보내기에 위의 함수를 작성한것이지 만약 cookie에 있는 토큰을 인증하고 싶다면, 다른함수를 사용하면 된다.

아래는 cookie에 담긴 refreshToken을 검증하는 Strategy로직이다.

@Injectable()
export class RefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
  constructor(private authService: AuthService) {
    super({
      //쿠키의 리프레시 토큰 검증
      jwtFromRequest: ExtractJwt.fromExtractors([
        (req: Request) => {
          return req?.cookies?.refresh;
        },
      ]),
      ignoreExpiration: false,
      // 리프레시 토큰 비밀키
      secretOrKey: process.env.REFRESH_JWT_SECRET,
    });
  }

위의 로직들 처럼 Guard와 Strategty를 작성하면 앞서 설명한 controller에서 라우터단위로 @UseGuard 데코레이터를 사용해서 req에서 strategy의 validate함수의 결과로 나오는 user를 얻을 수 있다.

 @UseGuards(RefreshTokenGuard)  // << @UseGuard 데코레이터 사용해서
  @HttpCode(HttpStatus.CREATED)
  @Post('/refresh-token')
  async ckeckRefresh(
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<Response> {
    const user: any = req.user;// << req.user 객사를 사용
    const payload: Payload = { id: user.id, nickname: user.nickname };
    const newJwt = await this.authService.createAccessToken(payload);

    res.setHeader('Authorizetion', 'Bearer ' + newJwt);

    return res.json({ msg: '토큰 재발급' });
  }

Guard에서 Strategy를 어떻게 찾는가? 에 대한 궁금증을 해결해 줄 포스팅
인프런 담변 JwtAuthGuard가 Strategy를 어떻게 알고 실행하는건가요?
티스토리 포스팅 한 NestJS 서버에서 여러 Strategy 구현하기
티스토리 [NestJS] AuthGuard는 어떻게 JwtStrategy를 찾는걸까? 마법인가?

profile
22년 12월 개발을 시작한 신입 개발자 ‘권태형’입니다. 포스팅 하나하나 내가 다시보기 위해 쓰는 것이지만, 다른 분들에게도 도움이 되었으면 좋겠습니다. 💯컬러폰트가 잘 안보이실 경우 🌙다크모드를 이용해주세요.😀 지적과 참견은 언제나 환영합니다. 많은 댓글 부탁드립니다.

0개의 댓글