NestJS에서 AccessToken, RefreshToken 구현

LeeJaeHoon·2022년 3월 5일
0
post-thumbnail

AccessToken이 만료됐을때 RefreshToken을 사용하여 AccessToken을 재발급 받기

사용자가 Login했을때 httpOnly Cookie에 AccessToken, RefreshToken넣어주기

필요한 함수

  • Cookie Option과 AccessToken을 return하는 함수
async getCookieWithJwtAccessToken(email: string) {
    const payload = { email };
    const token = this.jwtService.sign(payload, {
      secret: this.configService.get('JWT_SECRET'),
      expiresIn: `${this.configService.get(
        'JWT_ACCESS_TOKEN_EXPIRATION_TIME',
      )}s`,
    });
    return {
      accessToken: token,
      accessOption: {
        domain: 'localhost',
        path: '/',
        httpOnly: true,
        maxAge:
          Number(this.configService.get('JWT_ACCESS_TOKEN_EXPIRATION_TIME')) *
          1000,
      },
    };
  }
  • Cookie Option과 RefreshToken을 return하는 함수
  async getCookieWithJwtRefreshToken(email: string) {
    const payload = { email };
    const token = this.jwtService.sign(payload, {
      secret: this.configService.get('JWT_SECRET'),
      expiresIn: `${this.configService.get(
        'JWT_REFRESH_TOKEN_EXPIRATION_TIME', // 일주일
      )}s`,
    });
    return {
      refreshToken: token,
      refreshOption: {
        domain: 'localhost',
        path: '/',
        httpOnly: true,
        maxAge:
          Number(this.configService.get('JWT_REFRESH_TOKEN_EXPIRATION_TIME')) *
          1000,
      },
    };
  }
  • User의 currentHashedRefreshToken컬럼을 update해주는 함수
  async updateRefreshTokenInUser(refreshToken: string, email: string) {
    if (refreshToken) {
      refreshToken = await bcrypt.hash(refreshToken, 10);
    }
    await this.userRepository.update(
      { email },
      {
        currentHashedRefreshToken: refreshToken,
      },
    );
  }
  • 로그인 했을시 아이디 비번이 맞는지 check해주는 함수
  async login({ email, password }: LoginInputDto) {
    const user = await this.userRepository.findOne({ email });
    if (!user) {
      throw new UnauthorizedException('존재하지 않는 사용자입니다.');
    }
    const match = await bcrypt.compare(password, user.password);
    if (!match) {
      throw new UnauthorizedException('비밀번호가 틀립니다.');
    }
    //이메일 인증되지 않은 사용자 에러처리
    if (!user.verified) {
      throw new UnauthorizedException('이메일 인증을 해야합니다.');
    }
    const { accessToken, accessOption } =
      await this.getCookieWithJwtAccessToken(email);

    const { refreshToken, refreshOption } =
      await this.getCookieWithJwtRefreshToken(email);
    await this.updateRefreshTokenInUser(refreshToken, email);

    const returnUser = await this.userRepository
      .createQueryBuilder('user')
      .select([
        'user.id',
        'user.username',
        'user.email',
        'user.ksDepartment',
        'user.enterYear',
        'user.verified',
      ])
      .where('user.email = :email', { email })
      .getOne();
    return {
      accessToken,
      accessOption,
      refreshToken,
      refreshOption,
      user: returnUser,
    };
  }

Controller

  @Post('/login')
  async login(
    @Res({ passthrough: true }) res: Response,
    @Body(ValidationPipe) loginInputDto: LoginInputDto,
  ) {
    const { accessToken, accessOption, refreshToken, refreshOption, user } =
      await this.authService.login(loginInputDto);
    res.cookie('Authentication', accessToken, accessOption);
    res.cookie('Refresh', refreshToken, refreshOption);
    return { user };
  }

JWT 전략 세우기

accessToken전략

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(UserRepository)
    private readonly userRepository: UserRepository,
    private readonly configService: ConfigService,
  ) {
    super({
      secretOrKey: configService.get<string>('JWT_SECRET'),
      jwtFromRequest: ExtractJwt.fromExtractors([
        (request) => {
          return request?.cookies?.Authentication;
        },
      ]),
    });
  }

  async validate({ email }) {
    const user: User = await this.userRepository.findOne(
      { email },
      { select: ['id', 'ksDepartment', 'verified', 'username', 'email'] },
    );
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

RefreshToken전략

필요한 함수

  • RefreshToken이 로그인한 UserRefreshToken이 맞는지 확인하는 함수
  async getUserRefreshTokenMatches(
    refreshToken: string,
    email: string,
  ): Promise<{ result: boolean }> {
    const user = await this.userRepository.findOne({ email });
    if (!user) {
      throw new UnauthorizedException('존재하지 않는 사용자입니다.');
    }
    const isRefreshTokenMatch = await bcrypt.compare(
      refreshToken,
      user.currentHashedRefreshToken,
    );
    if (isRefreshTokenMatch) {
      return { result: true };
    } else {
      throw new UnauthorizedException();
    }
  }
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(
  Strategy,
  'jwt-refresh-token',
) {
  constructor(
    @InjectRepository(UserRepository)
    private readonly userRepository: UserRepository,
    private readonly configService: ConfigService,
    private readonly authService: AuthService,
  ) {
    super({
      secretOrKey: configService.get<string>('JWT_SECRET'),
      jwtFromRequest: ExtractJwt.fromExtractors([
        (request) => {
          return request?.cookies?.Refresh;
        },
      ]),
      passReqToCallback: true,
    });
  }

  async validate(req, { email }) {
    const refreshToken = req.cookies?.Refresh;
    console.log(refreshToken);
    await this.authService.getUserRefreshTokenMatches(refreshToken, email);
    const user: User = await this.userRepository.findOne(
      { email },
      { select: ['id', 'ksDepartment', 'verified', 'username', 'email'] },
    );
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

RefreshToken을 사용하여 AccessToken 재발급 받기

JwtRefreshTokenGuard만들기

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

@Injectable() 
export class JwtRefreshTokenGuard extends AuthGuard('jwt-refresh-token') {}

refreshAPI

  @Post('/refresh')
  @UseGuards(JwtRefreshTokenGuard)
  async refresh(
    @Res({ passthrough: true }) res: Response,
    @GetUser() user: User,
  ) {
    this.logger.verbose(`User: ${user.username} trying to refreshToken`);
    if (user) {
      const { accessToken, accessOption } =
        await this.authService.getCookieWithJwtAccessToken(user.email);
      res.cookie('Authentication', accessToken, accessOption);
      return { user };
    }
  }

Logout

필요한 함수

  • 만료시간이 0인 accessOption, refreshOption을 return 하는 함수
  getCookiesForLogOut() {
    return {
      accessOption: {
        domain: 'localhost',
        path: '/',
        httpOnly: true,
        maxAge: 0,
      },
      refreshOption: {
        domain: 'localhost',
        path: '/',
        httpOnly: true,
        maxAge: 0,
      },
    };
  }

logoutAPI

  @Get('/logout')
  @UseGuards(JwtRefreshTokenGuard)
  async logOut(
    @Res({ passthrough: true }) res: Response,
    @GetUser() user: User,
  ) {
    const { accessOption, refreshOption } =
      this.authService.getCookiesForLogOut();
    await this.authService.removeRefreshToken(user.email);
    res.cookie('Authentication', '', accessOption);
    res.cookie('Refresh', '', refreshOption);
  }

0개의 댓글