[NestJS]액세스 / 리프레시 토큰 저장 및 검증에서 만난 에러 (해결 방법 추가)

이경택·2023년 6월 12일

백엔드

목록 보기
1/5

access token, refresh 토큰 쿠키 저장 및 검증 방법 에러

  • 보통 토큰 처리 방식

보통은 액세스 토큰은 만료 주기를 짧게 해서 클라이언트에 전달, 클라이언트는 로컬 스토리지에 저장해서 데이터 요청 혹은 인증 요청 시 헤더에 포함해서 서버에 전달,

리프레시 토큰은 서버에서 클라이언트에 노출되지 않는 쿠키에 저장했다가 액세스 토큰 만료 시 쿠키의 리프레시 토큰을 db 의 리프레시 토큰과 비교,

→ 맞다면 새로운 액세스, 리프레시 토큰 발급,

→ 틀리거나 만료되었다면 로그아웃 시키는 방식으로 로그인과 인증을 구현하는 것으로 알고 있다.

  • 내가 생각한 방식

하지만 나는 액세스, 리프레시 토큰 모두 쿠키에 저장해 서버에서 처리를 한다면 클라이언트 단에서 따로 로컬 스토리지에 액세스 토큰을 저장하거나 저장한 토큰을 헤더에 담아 요청하는 번거로운 단계가 하나 없어지지 않을 까 생각하고 두 토큰 모두를 쿠키에 저장해서 서버에서 토큰 만료에 대한 처리를 하려고 생각했다.

만난 문제

NestJS에서는 jwt token 검증에 대한 방법으로 passport 라는 라이브러리를 사용한다.

이 라이브러리는 인증 관련 라이브러리로 module에 imports에 등록해서 사용할 수 있다.

그리고 strategy 파일과 그 strategy 파일에 작성한 인증 로직을 guard 파일에 등록하면 @UseGuard 에 등록하여 사용 할 수 있다.

@UseGuard 는 NestJS에서 인증 역할을 하는 미들웨어이다.

strategy 파일은 PassportStrategy 클래스를 확장시켜 사용하며 super 호출을 사용하여 기본 설정을 내가 원하는 대로 초기화 시킬 수 있다.

// jwt.strategy.ts
export class JwtStrategy extends PassportStrategy(Strategy,'jwt') {
  constructor() {
    super({
			// jwt 추출되는 방법
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
			// passport에 만료 여부 확인하는 책임 위임 여부 (false 가 만료 여부 확인)
      ignoreExpiration: false,
			// module에서 등록한 비밀키
      secretOrKey: jwtConstants.secret,
    });
  }
	// payload에는 jwt 토큰의 페이로드로 토큰에 포함된 정보를 담고 있다.
  async validate(payload: any) {
    return { userId: payload.userId, username: payload.username };
  }
}
// jwt.guard.ts
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {}

// auth.controller.ts
@Get('/authenticate')
  @UseGuards(AuthGuard('jwt'))
  isAuthenticated(@Res() {
    return 'success';
  }

이런식으로 strategy 파일이 작성되고 controller 파일에서 useGuard에 호출되어 사용된다.

여기서 @UseGuard 미들웨어에 액세스 토큰을 검증하는 strategy와 액세스 토큰이 만료되었을 때 리프레시 토큰을 검증하는 strategy를 같이 사용하려 했는데 실패했다.

구글링 해봤을 때 @UseGuard(가드1, 가드2) 을 controller에서 선언하고 가드1에 대한 확장 파일을 만들어서 가드1을 통과했을 때 가드2가 실행되게끔 만드는 로직을 만들 수 있는 것 같았는데 잘 되지 않았다.

예시 코드

// local.guard.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ExecutionContext } from '@nestjs/common';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    try {
      await super.canActivate(context);
      return true;
    } catch (err) {
      // JwtStrategy에서 발생한 에러를 처리하거나 무시
      throw new UnauthorizedException();
    }
  }
}

jwt guard를 확장한 것으로 클래스 내부에 canActivate라는 메서드를 이용해 상위 클래스의 검증이 error가 나지 않는다면 true를 반환하고 에러가 난다면 catch 문에서 처리하거나 무시할 수도 있다.

→ 사실 내가 정확하게 맞게 이해하고 사용했다면 내가 원하는 대로 값이 나왔을텐데 내가 잘못 알고 있는 부분이 있는 것 같다.

해결한 방법

  1. 액세스 토큰은 로그인 시에 클라이언트 단으로 넘겨 로컬 스토리지에 저장하게 만들고, 리프레시 토큰은 헤더에 저장해, 액세스 토큰이 만료되었을 때 리프레시 토큰을 확인하는 함수를 호출하도록 하고, 확인 여부에 따라 로그인, 로그아웃 시킨다.
  2. Nest.JS 의 CanActive 메서드를 이용해 request 객체를 얻어내고, request 객체에서 쿠키에 접근해 accessToken, refreshToken을 검증한다.
// auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { Request } from 'express';
import { ConfigService } from '@nestjs/config';

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

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new NotFoundException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET_KEY'),
      });
      request['user'] = payload;
    } catch {
      try {
        const refresh = this.extractRefreshTokenFromHeader(request);
        const res = await this.jwtService.verifyAsync(refresh, {
          secret: this.configService.get('JWT_REFRESH_TOKEN_SECRET_KEY'),
        });
        if (res) {
          // 리프레시 토큰 업데이트 및 업데이트 된 토큰 return
        }
      } catch (error) {
        throw new NotFoundException(error);
      }
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const access = request.headers?.cookie.replace('access=', '');
    return access ? access : undefined;
  }

  private extractRefreshTokenFromHeader(request: Request): string | undefined {
    const refresh = request.headers?.cookie.replace('refresh=', '');
    return refresh ? refresh : undefined;
  }
}

즉, useGuards 에 여러 가드를 등록하는 것이 아닌 하나의 가드에서 쿠키에 있는 액세스 토큰과 리프레시 토큰의 만료 여부를 확인해서 만료 여부에 따른 에러 처리를 해주는 방식으로 프론트에서 토큰을 넣어주지 않아도 user의 정보를 원할 때 api 요청만 한다면 백엔드에서 알아서 처리하는 방식으로도 해결할 수 있었다.

3 -> 4줄 요약

  1. 일반적인 방식이 아닌 내가 생각한 방식(액세스, 리프레시 토큰 둘 다 쿠키에 저장 및 서버에서 처리) 시도
  2. 공식 문서와 블로그등을 찾아보며 여러 방식을 시도해봤지만 내가 원하는 결과 나오지 않음
  3. 일반적인 방식으로 처리
  4. 쿠키의 accessToken, refreshToken 확인 후 검증 및 갱신 처리
profile
한 줄로 소개 할 수 없는 개발자

0개의 댓글