Node.js AccessToken / RefreshToken

2024. 5. 21. 14:02Node.js

728x90
반응형

 

AccessToken에 개념을 이해하고 사용을 하고 있었지만, 이번에 내일배움캠프를 수강 중 RefreshToken을 배워 개념과 간단하게 사용하는 방법을 알아보려고 합니다.

 

JWT(Json Web Token)?

Header
JWT를 검증하는 데 필요한 정보를 가진 JSON을 Base64 알고리즘 기반으로 인코딩한 문자열입니다. 검증을 하기 위한 내용을 가지고 있습니다.

Payload
JWT에 저장된 값입니다. (name, value)의 쌍으로 이루어져 있고, JWT에는 이 값들을 여러 개 할당할 수 있습니다. Payload의 값은 암호화되지 않기에, 비밀번호와 같은 민감한 값을 넣으면 안 됩니다.

Signature
JWT를 인코딩하거나 유효성 검증을 할 때 사용하는 암호화 된 코드입니다.

 

 

1. Acess Token만을 이용한 서버 인증 방식

  • 사용자가 로그인
  • 서버에서 사용자 확인 후, Access Token(JWT)에 권한 인증을 위한 정보를 Payload에 넣고 생성
  • 생성한 토큰을 클라이언트에게 반환하고, 클라이언트는 받은 토큰을 저장
  • 클라이언트는 권한 인증이 필요한 요청을 할 때마다 이 토큰을 헤더(쿠키)에 전송
  • 서버는 헤더의 토큰을 검증하고, Payload의 값을 디코딩하여 사용자의 권한을 확인하고 데이터를 반환합니다.
  • 만약, 토큰이 valid하지 않거나 만료되었다면 새로 로그인을하여 토큰을 발급받아야합니다.

2. Refresh Token

Refresh Token은 Access Token이 탈취당하거나 유효기간이 만료되었을 때 사용됩니다. Access Token은 유효기간이 짧은 대신에, Access Token이 만료되면 클라이언트에서는 Refresh Token을 서버에 전송해 사용자인증을 하고 Access Token을 재발급받아 전송받습니다.

 

 

import express from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import { userLoginSchema } from '../middlewarmies/validation.middleware.js';
import { prisma } from "../utils/prisma.util.js";
import { catchAsync } from '../middlewarmies/error-handler.middleware.js';
import { AUTH_MESSAGES } from '../constants/auth.constant.js';
import { SECRET_KEY, JWT_EXPIRATION_TIME, REFRESH_SECRET_KEY, REFRESH_TOKEN_EXPIRATION_TIME} from '../constants/env.constant.js';


authRouter.post('/sign-in', catchAsync(async (req, res) => {
  const data = req.body;
  const { error } = userLoginSchema.validate(data);
  if (error) return res.status(400).json({ message: error.message });

  const userData = await prisma.user.findFirst({ where: { email: data.email } });  
  if(!userData) return res.status(401).json({ message: AUTH_MESSAGES.INVALID_CREDENTIALS });

  const isMatched = await bcrypt.compare(data.password, userData.password);
  if (!isMatched) return res.status(401).json({ message: AUTH_MESSAGES.INVALID_CREDENTIALS });

  const accessToken = jwt.sign(
    { id: userData.id,
      role: userData.role
     },
    SECRET_KEY,
    { expiresIn: JWT_EXPIRATION_TIME }
  );

    // RefreshToken 생성
    const refreshToken = jwt.sign(
      { id: userData.id, role: userData.role },
      REFRESH_SECRET_KEY,
      { expiresIn: REFRESH_TOKEN_EXPIRATION_TIME }
    );
  
    // DB에 리프레시 토큰 저장
    await prisma.user.update({
      where: { id: userData.id },
      data: {
        refreshToken: refreshToken
      }
    });

  return res.status(200).json({ accessToken, refreshToken});
}));

 

위에 코드는 현재 사용자가 email, password를 작성하였을 때 토큰을 생성하고 발급하는 로그인API이다. 

 

acessToken 미들웨어

/** 엑세스 토큰 검증 API **/
const authMiddleware = catchAsync(async (req, res, next) => {
    const accessToken = req.cookies.accessToken;
    if (!accessToken || !accessToken.startsWith('Bearer ')) {
      return res.status(400).json({ errorMessage: AUTH_MESSAGES.NO_AUTH_INFO });
    }
    const token = accessToken.split(' ')[1];
    const payload = await validateToken(token, SECRET_KEY);
    console.log(payload)
    if (payload === null) {
      // 액세스 토큰이 만료되었을 경우
      const refreshToken = req.headers.authorization;
      if (!refreshToken || refreshToken === null) {
        // 리프레시 토큰 요청
        return res.status(401).json({ errorMessage: AUTH_MESSAGES.REFRESH_TOKEN_REQUIRED });
      } else {
        // 헤더에 리프레시 토큰이 있을 경우, 리프레시 미들웨어로 이동
        return refreshTokenMiddleware(req, res, next);
      }
    }

    if (!payload) {
      return res.status(401).json({ errorMessage: AUTH_MESSAGES.INVALID_AUTH });
    }
    const user = await prisma.user.findUnique({
      where: { id: payload.id }
    });
    if (!user) {
      return res.status(404).json({ errorMessage: AUTH_MESSAGES.USER_NOT_FOUND });
    }
    req.user = user;
    next();
});

const validateToken = async (token, secretKey) => {
  try {
    const payload = jwt.verify(token, secretKey);
    return payload;
  } catch (error) {
    return null;
  }
}

export { authMiddleware, validateToken };

 

 

Refresh Token 미들웨어

/** 리프래시 토큰 검증 및 재발급 미들웨어 **/
const refreshTokenMiddleware = catchAsync(async (req, res, next) => {
  // 쿠키에서 리프래시 토큰을 가져옴
  const refreshToken = req.headers.authorization;
  if (!refreshToken) {
    return res
      .status(400)
      .json({ errorMessage: AUTH_MESSAGES.NO_REFRESH_TOKEN });
  }

  // 리프래시 토큰 검증
  const payload = jwt.verify(refreshToken, REFRESH_SECRET_KEY);
  if (!payload) {
    return res
      .status(401)
      .json({ errorMessage: AUTH_MESSAGES.INVALID_REFRESH_TOKEN });
  }

  const data = await prisma.user.findFirst({
    where: {
      refreshToken: refreshToken,
    },
  });
  if (!data)
    return res.status(400).json({ errorMessage: AUTH_MESSAGES.TOKEN_END });

  // 사용자 조회
  const user = await prisma.user.findUnique({
    where: { id: payload.id },
  });
  if (!user) {
    return res.status(404).json({ errorMessage: AUTH_MESSAGES.USER_NOT_FOUND });
  }

  // 액세스 토큰 발급 (새로고침)
  const newAccessToken = jwt.sign({ id: user.id }, REFRESH_SECRET_KEY, {
    expiresIn: '15m',
  });

  // 새로운 액세스 토큰을 쿠키에 설정
  // 12시간을 밀리초로 변환
  // const maxAge = 12 * 60 * 60 * 1000;

  res.cookie('accessToken', newAccessToken);

  // 요청에서 사용자 정보를 전달
  req.user = user;

  // 다음 미들웨어로 이동
  next();
});

export { refreshTokenMiddleware };

 

728x90
반응형