dh_0e

[Node.js] 강의 내용 개념 정리(6) (Access Token, Refresh Token) 본문

내일배움캠프/Node.js[숙련]

[Node.js] 강의 내용 개념 정리(6) (Access Token, Refresh Token)

dh_0e 2024. 5. 27. 20:45

Access Token

  • 사용자의 인증이 완료된 후 해당 사용자를 인증하는 용도로 발급하는 토큰(쿠키에  jwt를 설정하고 만료 시간이 지나면 인증이 만료되는 구조 또한 Access Token)
  • 토큰을 생성할 때 사용한 비밀키로 인증을 처리
  • Stateless(무상태) 즉, Node.js 서버가 재시작되더라도 동일하게 작동함
  • 이는 jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만, 처음 토큰을 발급한 사용자인지 확인할 수는 없음
  • 그 자체로 사용자 인증에 필요한 모든 정보를 가지고 있음
  • 토큰을 가지고 있는 시간이 늘어날 수록, 탈취되었을 때 피해 규모가 더욱 커짐

 

Refresh Token

  • 사용자의 모든 인증 정보를 담고있는 Access Token과 달리 특정 사용자가 Access Token을 발급받기 위한 목적으로만 사용됨
  • 디코딩하여 사용자의 정보를 확인하며 필요한 경우 서버에서 강제로 토큰을 만료시킬 수 있어 사용자의 인증 상태를 언제든지 서버에서 제어할 수 있다는 장점을 가지고 있음
  • OTP처럼 사용자의 인증 정보는 짧은 시간동안만 사용되도록 제한하여 피해를 최소화함

Refresh Token으로 Access Token을 재발급 받음

ex)

// app.js

import express from "express";
import jwt from "jsonwebtoken";
import cookieParser from "cookie-parser";

const app = express();
const PORT = 3019;

// AccessToken을 발급하는 함수
function createAccessToken(id) {
  const accessToken = jwt.sign({ id }, ACCESS_TOKEN_SECRET_KEY, {
    expiresIn: "10s",
  });
  return accessToken;
}

const ACCESS_TOKEN_SECRET_KEY = `Secret Key for Access Token`; // Access Token의 비밀 키를 정의합니다.
const REFRESH_TOKEN_SECRET_KEY = `Secret Key for Refresh Token`; // Refresh Token의 비밀 키를 정의합니다.

app.use(express.json());
app.use(cookieParser());

app.get("/", (req, res) => {
  return res.status(200).send("Hello Token!");
});

const tokenStorages = {}; // 리프레시 토큰을 관리할 객체

/* 엑세스, 리프레시, 토큰 발급 API */
app.post("/tokens", async (req, res) => {
  const { id } = req.body;

  //엑세스 토큰과 리프레시 토큰을 발급
  const accessToken = createAccessToken(id);
  const refreshToken = jwt.sign({ id }, REFRESH_TOKEN_SECRET_KEY, {
    expiresIn: "7d",
  });

  tokenStorages[refreshToken] = {
    id,
    ip: req.ip,
    userAgent: req.headers["user-agent"], // 특정 클라이언트가 어떤 방식으로 서버에 요청을 했는지
  };

  // 클라이언트에게 쿠키(토큰)을 할당
  res.cookie("accessToken", accessToken);
  res.cookie("refreshToken", refreshToken);

  return res.status(200).json({ message: "토큰이 정상적으로 발급됨" });
});

/* Access Token 검증 API */
app.get("/tokens/validate", async (req, res) => {
  const { accessToken } = req.cookies;

  //Access Token이 존재하는지 확인
  if (!accessToken) {
    return res
      .status(401)
      .json({ errorMessage: "Access Token이 존재하지 않음" });
  }

  const payload = validateToken(accessToken, ACCESS_TOKEN_SECRET_KEY);

  if (!payload) {
    return res
      .status(401)
      .json({ errorMessage: "Access Token이 정상적이지 않음" });
  }

  const { id } = payload;

  return res.status(200).json({
    message: `${id}의 Payload를 가진 Token이 정상적으로 인증 되었습니다.`,
  });
});

// 토큰을 검증하고, Payload를 조회하기 위한 함수
function validateToken(token, secretKey) {
  try {
    return jwt.verify(token, secretKey);
  } catch {
    return null;
  }
}

/* Refresh Token을 이용해서 Access Token을 재발급 받는 API */
app.post("/tokens/refresh", async (req, res) => {
  const { refreshToken } = req.cookies;

  if (!refreshToken) {
    return res
      .status(400)
      .json({ errorMessage: "Refresh Token이 존재하지 않습니다." });
  }

  const payload = validateToken(refreshToken, REFRESH_TOKEN_SECRET_KEY);

  if (!payload) {
    return res
      .status(401)
      .json({ errorMessage: "Refresh Token이 정상적이지 않습니다." });
  }

  const userInfo = tokenStorages[refreshToken];
  if (!userInfo) {
    return res
      .status(419)
      .json({ errorMessage: "Refresh Token이 서버에 존재하지 않습니다." });
  }

  const { id } = payload;
  const accessToken = createAccessToken(id);

  res.cookie("accessToken", accessToken);

  return res.status(200).json({ message: "Access Token 재발급 완료" });
});
app.listen(PORT, () => {
  console.log(PORT, "포트로 서버가 열렸어요!");
});

 

  • tokenStorage 객체처럼 리프레시 토큰을 저장할 객체는 로컬이 아닌 아래와 같이 db에 테이블을 따로 만들어 저장하는 것이 좋다.

이 외에도 ip 또는 user-Agent와 같은 다양한 정보를 추가하면 보안성이 올라감

 

user-agent: 요청한 클라이언트 정보를 알 수 있음

req.headers["user-agent"]