개발일지

[NestJS] Refresh token으로 Access token 재발급하기 본문

NestJS, Node.js/#01 Project - 투표 커뮤니티

[NestJS] Refresh token으로 Access token 재발급하기

lyjin 2022. 10. 28.

시나리오

제가 생각한 access token 재발급 순서는 다음과 같습니다.

  1. access token 만료시 error를 반환합니다.
  2. 클라이언트는 body refresh token를 포함한 뒤 access token 재발급을 요청합니다.
  3. 서버에서는 refresh token을 검증합니다.
    1. db에 저장된 refresh token과의 일치 여부를 확인합니다.
    2. refresh token의 유효성을 검증합니다.
  4. 검증된 refresh token의 경우 access token을 재생성해 response 합니다.

 

 

참고) Refresh token 생명 주기

  • 로그인 시 access token과 refresh token이 발급됩니다.
  • 로그아웃 시 db에 저장된 refresh token이 삭제됩니다.
  • 보안성을 위해 refresh token은 암호화 된 상태로 db에 저장되고 reponse 되어야 합니다.

 

 


Refresh token 검증 구현

DB에 저장된 token 값과의 일치여부 확인

컨트롤러에서 @CurrUser()로 받아온 userId와 refreshToken, 두 데이터와 일치하는 값을 가진 RefreshTokens를 찾습니다. 존재하지 않을 경우 error를 던집니다.

 

// users/users.service.ts
// class TokenService

  async verifyRefreshToken(
    userId: number,
    encryptRefreshToken: string,
  ): Promise<VerifiedToken> {
    let verifiedToken: VerifiedToken;

   // 1-2. 유효성 검증 - refresh token db 검증
    const token: RefreshTokens = await this.usersRepository.findRefreshToken(
      encryptRefreshToken,
    );

    if (!token) {
      throw new CustomException(UsersException.UNVERIFIED_REFRESH_TOKEN);
    }

    ...
  }

 

  async findMatchedRefreshToken(
    userId: number,
    encryptRefreshToken: string,
  ): Promise<RefreshTokens> {
    return await this.prisma.refreshTokens.findFirst({
      where: {
        userId: userId,
        token: encryptRefreshToken,
      },
    });
  }

 

Refresh token 검증

access token과 마찬가지로 @nestjs/jwt.verify() 함수를 사용해 검증합니다. 이때 클라이언트로부터 받아온 토큰은 암호화된 상태입니다. 따라서 .verify() 인수로 넣기 위해서는 복호화 해줘야합니다.

 

  async verifyRefreshToken(...): Promise<VerifiedToken> {
    // 1-2. 유효성 검증 - refresh token db 검증
    ...

    // 1-3. 유효성 검증 - refresh token 검증
    const decryptRefreshToken: string =  // 복호화
      this._decryptRefreshToken(encryptRefreshToken);

    try {
      verifiedToken = this.jwtService.verify(decryptRefreshToken, {
        secret: 'refresh-secret-key',
      });
    } catch (error) {
      switch (error.message) {
        case 'jwt expired':
          throw new CustomException(UsersException.EXPIRED_TOKEN);
        default:
          throw new CustomException(UsersException.UNVERIFIED_REFRESH_TOKEN);
      }
    }

    return verifiedToken;
  }

 

Access token 재발급

완성된 recreateAccessToken service입니다. verifiedUser는 .verify() 된 결과 값으로 검증된 refresh token의 페이로드입니다. 이 페이로드로 access token을 생성해줍니다.

 

// users/users.service.ts
// class UsersService

  async recreateAccessToken(
    userId: number,
    encryptRefreshToken: string,
  ): Promise<RecreateAccessToken> {
      // 1-1. 유효성 검증 - resuest로부터 token을 받지 못한 경우
    if (!encryptRefreshToken) {
      throw new CustomException(UsersException.TOKEN_NOT_EXISTS);
    }

    const verifiedUser: VerifiedToken =
      await this.tokenService.verifyRefreshToken(userId, encryptRefreshToken);

    const payload: JwtPayload = {
      sub: verifiedUser.sub,
      nickname: verifiedUser.nickname,
    };

    const accessToken = this.tokenService.createAccessToken(payload);
    return { accessToken };
  }

 

users/users.controller.ts

  @UseGuards(JwtAccessGuard)
  @Post('recreate/access-token')
  async recreateAccessToken(
    @CurrUser('id', ParseIntPipe) userId: number,
    @Body('refreshToken') encryptRefreshToken: string,
  ): Promise<RecreateAccessToken> {
    return await this.usersService.recreateAccessToken(
      userId,
      encryptRefreshToken,
    );
  }