Nest.js에서 JWT Guard 사용하기 (1)

Sinf·2022년 6월 29일
2

고민의 흔적

목록 보기
30/38
post-thumbnail

이전 포스팅에서 Nest.js에서 passport를 활용해 Google login을 적용했다.

Google login을 통해 구글 계정 정보를 가져왔지만, 이를 매번 요청해서 토큰을 사용하기보다, 서버 자체에서 정보를 저장하고 토큰을 발급하는 것이 불필요한 리다이렉션을 줄일 수 있다고 생각했다.

흐름 파악하기

내가 생각한 JWT을 활용한 인증 단계는 다음과 같다.

  1. Google Login 이후 Token 발급
  2. 로그인이 필요한 서비스에 Token 검증을 위한 Guard
  3. 만료 시 토큰 갱신 혹은 다시 로그인

Goolge login 이후 Token 발급

이전 포스팅에서 Google login 이후 callbackURL에 맞게 Controller를 작성해 req.user로 Google profile 값을 가져왔다.

이제 profile 값으로 현재 서버에 유저가 저장되어 있는지 확인하고, 있다면 토큰 발급, 없다면 유저를 저장하고 토큰을 발급한다.

Google에서 발급한 ProviderId는 유일한 값이므로 해당 값으로 유저를 조회한다.

먼저, 구글 유저 정보를 받아 유저 정보 존재 유무에 따라 저장 혹은 패쓰하는 메서드를 작성한다.

// GoogleUser type
interface GoogleUser {
  provider: string;
  providerId: string;
  email: string;
  name: string;
}

// findByProviderIdOrSave in UserService
async findByProviderIdOrSave(googleUser: GoogleUser) {
  const { providerId, provider, email, name } = googleUser;

  const user = await this.userRepository.findOne({ where: { providerId } });

  if (user) {
    return user;
  }

  const newUser = new UserEntity();
  newUser.provider = provider;
  newUser.providerId = providerId;
  newUser.email = email;
  newUser.name = name;

  return await this.userRepository.save(newUser);
}

조회를 통해 유저가 있다면 유저를 리턴하고, 없다면 저장 후 리턴하도록 했다.

이제 유저 정보가 DB에 있으니 토큰을 발급한다.

// type JwtPayload
interface JwtPayload {
  sub: string;
  email: string;
}

// callback in AuthController
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleCallback(@Req() req: Request, @Res() res: Response) {
  const user = await this.userService.findByProviderIdOrSave(
    req.user as GoogleUser,
  );

  const payload: JwtPayload = { sub: user.id, email: user.email };

  const { accessToken, refreshToken } = this.authService.getToken(payload);

  // ...
}

토큰을 생성하기 위한 Payload를 지정하고, 토큰을 발급한다.
getToken 메서드는 다음과 같다.

getToken(payload: JwtPayload) {
  const accessToken = this.jwtService.sign(payload, {
    expiresIn: '2h',
    secret: process.env.JWT_SECRET,
  });

  const refreshToken = this.jwtService.sign(payload, {
    expiresIn: '7d',
    secret: process.env.JWT_SECRET,
  });

  return { accessToken, refreshToken };
}

토큰을 발급할 때, jsonwebtoken 라이브러리를 통해 직접 발급할 수 있다. Nest.js에서는 @nestjs/jwt 라이브러리를 통해 JWT을 위한 서비스 클래스를 제공한다.

@nestjs/jwt 사용하기

@nestjs/jwt 라이브러리를 설치한다.

yarn add @nestjs/jwt # npm install @nestjs/jwt

JwtModule을 AuthModule에 import 한다.

import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [JwtModule.register({})],
})
export class AuthModule {}

register 메서드에 옵션을 전달해 토큰 발급에 대한 옵션을 전달할 수 있지만, 토큰을 생성할 때 직접 전달하도록 한다. (access, refresh 차이를 확인하기 좋을 것이라고 생각한다.)

export class AuthService {
  constructor(private jwtService: JwtService) {}
}

이제 JwtService를 생성자 주입을 통해 사용하고자 하는 클래스에 주입하면 JWT와 관련된 메서드를 사용할 수 있다.

Access Token and Refresh Token

JWT를 생성하는데, 두 개의 토큰을 생성하는 것을 볼 수 있다.
처음 JWT를 사용할 땐, 저런 개념 없이 Token을 발급하고 사용했었다. 왜 다른 두 토큰을 사용할까?

요약해서 말하자면, 쿠키-세션 기반 인증 방식에 비해 JWT 기반의 인증 방식은 토큰 노출에 대한 위험이 높다.

그래서, 만료 시한이 짧은 Access Token과 만료 시한이 비교적 긴 Refresh Token을 발급한다.

인증에 사용되는 Access Token이 노출되더라도, 짧은 시간 내에 만료시킨다. 그리고 Access Token이 만료된 경우 Refresh Token을 통해 유저의 로그인 없이 서비스를 지속할 수 있도록 한다.

해당 내용은 더 많은 설명이 필요하다.

다시 이어서 토큰 발급

이제 생성된 토큰을 클라이언트에 전달할 차례이다.

  1. 쿠키에 담아서 전달한다.
  2. 응답 Body에 담아서 전달한다.

Google login 없이 로컬 로그인을 사용한다면, 페이지에 대한 리다이렉션이 없기 때문에 응답 Body에 담아 토큰을 저장하는 것이 유용하다고 생각한다.

하지만 Google login을 사용하면 페이지의 리다이렉션 (구글 로그인 페이지)이 있기 때문에 해당 값을 응답으로 받아 처리하기 어렵다. 그래서 cookie로 받아오기로 결정했다.

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleCallback(@Req() req: Request, @Res() res: Response) {
    // ...

    res.cookie('access-token', accessToken);
    res.cookie('refresh-token', refreshToken);

    await this.userService.updateHashedRefreshToken(user.id, refreshToken);

    res.redirect(process.env.DOMAIN);
  }

그리고, 로그인이 완료되면 클라이언트의 Home으로 리다이렉션한다.

여기서 잠깐, updateHashedRefreshToken?

Access Token, Refresh Token을 생성한 후, Refresh Token은 유저 정보에 저장해야한다. (Access Token 만료 시 Refresh Token 비교 후 Access Token 재발급) 보안을 위해 해시 값으로 저장하고 해시 값으로 비교하도록 한다.

결론

  1. Google login 후 받아온 Profile을 통해 유저 정보를 저장하거나 조회한다.
  2. 유저 정보를 통해 Access Token과 Refresh Token을 발급한다. (@nestjs/jwt 라이브러리를 사용한다.)
  3. Refresh Token은 해시 값으로 유저 정보에 저장한다. (Access 만료 시 Refresh Token 검증)
  4. 생성된 Token을 클라이언트에 전달

Access Token과 Refresh Token에 대해서 많은 정보가 존재한다. 다음 포스팅에서 로그인이 필요한 API에 접근 시 어떤 과정으로 진행되는지, 과정에 따라 Guard를 어떻게 설정하는지 구현하도록 하자.

profile
주니어 개발자입니다. 🚀

0개의 댓글