프로젝트

트러블슈팅 3 .refreshToken 재발급

늘곰's 2023. 10. 10. 09:54

리프레시 토큰을 재발 급 받는 과정에서의 트러블 슈팅 

서비스 부분에서 리프레시 토큰을 발급받는 로직을 우선적으로 만듬

import {
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from './../prisma/prisma.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import {
  IAuthServiceGetAccessToken,
  IAuthServiceGetRefereshToken,
  IAuthServiceLogin,
} from './interface/auth-service.interface';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private prisma: PrismaService,
    private jwtService: JwtService,
  ) {}

  async login({ email, password }: IAuthServiceLogin): Promise<{
    accessToken: string;
    refreshToken: string;
  }> {
    // 1. 이메일이 일치하는 유저를 DB에서 찾기
    const user = await this.usersService.findByEmail({ email });

    // 2. 일치하는 유저가 없으면 에러
    if (!user) throw new NotFoundException('이메일이 없습니다.');

    // 3. 일치하는 유저는 있지만 비밀번호가 틀렸다면 에러
    const isAuth = await bcrypt.compare(password, user.password);
    if (!isAuth)
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');

    // 4. 리프레시 토큰 생성
    const refreshToken = this.generateRefreshToken({ user });

    // 5. 리프레시 토큰을 DB 또는 다른 안전한 저장소에 저장 (여기서는 저장하지 않고 클라이언트로 직접 보냄)

    // 6. 액세스 토큰 및 리프레시 토큰을 반환
    const accessToken = await this.getAccessToken({ user });

    return { accessToken, refreshToken };
  }

  async getAccessToken({ user }: IAuthServiceGetAccessToken): Promise<string> {
    const accessToken = this.jwtService.sign(
      { sub: user.userId },
      // secret 및 expiresIn 설정을 JwtModule.register에서 관리하도록 수정
      { secret: process.env.JWT_ACCESS_KEY, expiresIn: '15s' },
    );

    return accessToken;
  }

  generateRefreshToken({ user }: IAuthServiceGetRefereshToken): string {
    // 리프레시 토큰을 생성하는 로직을 구현
    const refreshToken = this.jwtService.sign(
      { sub: user.userId },
      { secret: process.env.JWT_REFRESH_KEY, expiresIn: '2w' },
    );
    return refreshToken;
  }

  async refreshAccessToken(refreshToken: string): Promise<string> {
    try {
      // 리프레시 토큰의 유효성을 검증
      const decodedToken = this.jwtService.verify(refreshToken, {
        secret: process.env.JWT_REFRESH_KEY,
      });

      // 리프레시 토큰이 유효하다면 새로운 액세스 토큰을 발급
      const newAccessToken = await this.getAccessToken({ user: decodedToken });

      return newAccessToken;
    } catch (error) {
      // 검증 실패 시 (예: 서명이 유효하지 않은 경우) 에러 처리
      throw new UnauthorizedException('Invalid refresh token');
    }
  }
}

그 후에 컨트롤러 부분에서 코드를 처리하던중 headers에서 정보가 제대로 안들어오고 있었다..

'{ new (init?: HeadersInit): Headers; prototype: Headers; }' 형식의 값은 호출할 수 없습니다. 'new'를 포함하려고 했습니까?ts(2348)
이러한 오류가 생기면서 값이 제대로 들어오지 않았음.

import { Body, Controller, Post, Res } from '@nestjs/common';
import { AuthService } from './auth.service';
import {
  ApiOkResponse,
  ApiOperation,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { AuthEntity } from './entity/auth.entity';
import { LoginDto } from './dto/login.dto';
import { Response } from 'express';

@Controller('users')
@ApiTags('Auth')
@ApiOkResponse({ type: AuthEntity })
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @ApiOperation({ summary: '로그인' })
  @ApiResponse({ status: 200, description: '로그인에 성공하셨습니다.' })
  @ApiResponse({ status: 400, description: '이메일이 없습니다.' })
  @ApiResponse({ status: 401, description: '비밀번호가 일치하지 않습니다.' })
  @Post('login')
  async login(
    @Body() { email, password }: LoginDto,
    @Res({ passthrough: true }) res: Response, // Response 객체 주입
  ): Promise<void> {
    const { accessToken, refreshToken } = await this.authService.login({
      email,
      password,
    });

    // 엑세스 토큰을 HTTP 응답 헤더에 추가
    res.header('access-token', accessToken);

    // 리프레시 토큰을 쿠키로 설정하여 클라이언트에게 전달
    //httpOnly : javascript 로 쿠키에 접근 할 수 없는 옵션
    //secure : true 일 시 https 연결에서만 전송된다.
    //리프레시  토큰을 HTTP 응답 헤더에 추가
    res.header('refreshToken', refreshToken);

    //res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: false });

    // 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
    res.status(200).json({ accessToken }); // 클라이언트에게 JSON 응답을 보냄
  }

  // 리프레시 토큰을 사용하여 엑세스 토큰 재발급을 위한 엔드포인트 추가
  @ApiOperation({ summary: '리프레시 토큰을 사용하여 엑세스 토큰 재발급' })
  @ApiResponse({
    status: 200,
    description: '엑세스 토큰 재발급에 성공하셨습니다.',
  })
  @ApiResponse({
    status: 401,
    description: '리프레시 토큰이 유효하지 않습니다.',
  })
  // ...
  @Post('refresh')
  async refreshAccessToken(
    @Headers('refresh-token') refreshToken: string, // 요청 헤더에서 refresh-token 값을 추출
    @Res({ passthrough: true }) res: Response,
  ): Promise<void> {
    try {
      // 리프레시 토큰을 사용하여 새로운 엑세스 토큰 발급
      const newAccessToken =
        await this.authService.refreshAccessToken(refreshToken);

      // 새로운 엑세스 토큰을 HTTP 응답 헤더에 추가
      res.header('access-token', newAccessToken);

      // 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
      res.status(200).json({ accessToken: newAccessToken });
    } catch (error) {
      // 리프레시 토큰이 유효하지 않은 경우 에러 처리
      res
        .status(401)
        .json({ errorMessage: '리프레시 토큰이 유효하지 않습니다.' });
    }
  }

  // ...
}

위에 Headers 를 응답을 받아오면서 해결
하지만 왜 기존처럼 컨트롤 + i로 응답이 안받아 졌던 건지는 모르겠음...
빠른수정에서 호출에 누락된new 연산자를 추가하겠냐는 말뿐이고 헤더를 추가해주는건 없어서 
찾는데 시간이 걸린것 같다. 

import { Body, Controller, Post, Res, Headers } from '@nestjs/common'; // Headers 추가
import { AuthService } from './auth.service';
import {
  ApiOkResponse,
  ApiOperation,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { AuthEntity } from './entity/auth.entity';
import { LoginDto } from './dto/login.dto';
import { Response } from 'express';

@Controller('users')
@ApiTags('Auth')
@ApiOkResponse({ type: AuthEntity })
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @ApiOperation({ summary: '로그인' })
  @ApiResponse({ status: 200, description: '로그인에 성공하셨습니다.' })
  @ApiResponse({ status: 400, description: '이메일이 없습니다.' })
  @ApiResponse({ status: 401, description: '비밀번호가 일치하지 않습니다.' })
  @Post('login')
  async login(
    @Body() { email, password }: LoginDto,
    @Res({ passthrough: true }) res: Response, // Response 객체 주입
  ): Promise<void> {
    const { accessToken, refreshToken } = await this.authService.login({
      email,
      password,
    });

    // 엑세스 토큰을 HTTP 응답 헤더에 추가
    res.header('access-token', accessToken);

    // 리프레시 토큰을 쿠키로 설정하여 클라이언트에게 전달
    // httpOnly : javascript 로 쿠키에 접근 할 수 없는 옵션
    // secure : true 일 시 https 연결에서만 전송된다.
    // 리프레시 토큰을 HTTP 응답 헤더에 추가
    res.header('refreshToken', refreshToken);

    // res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: false });

    // 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
    res.status(200).json({ accessToken }); // 클라이언트에게 JSON 응답을 보냄
  }

  // 리프레시 토큰을 사용하여 엑세스 토큰 재발급을 위한 엔드포인트 추가
  @ApiOperation({ summary: '리프레시 토큰을 사용하여 엑세스 토큰 재발급' })
  @ApiResponse({
    status: 200,
    description: '엑세스 토큰 재발급에 성공하셨습니다.',
  })
  @ApiResponse({
    status: 401,
    description: '리프레시 토큰이 유효하지 않습니다.',
  })
  @Post('refresh')
  async refreshAccessToken(
    @Headers('refreshToken') refreshToken: string, // 요청 헤더에서 refresh-token 값을 추출
    @Res({ passthrough: true }) res: Response,
  ): Promise<void> {
    try {
      // 리프레시 토큰을 사용하여 새로운 엑세스 토큰 발급
      const newAccessToken =
        await this.authService.refreshAccessToken(refreshToken);

      // 새로운 엑세스 토큰을 HTTP 응답 헤더에 추가
      res.header('access-token', newAccessToken);

      // 액세스 토큰을 클라이언트에게 JSON 응답으로 반환
      res.status(200).json({ accessToken: newAccessToken });
    } catch (error) {
      // 리프레시 토큰이 유효하지 않은 경우 에러 처리
      res
        .status(401)
        .json({ errorMessage: '리프레시 토큰이 유효하지 않습니다.' });
    }
  }
}