TIL - access Token & refresh Token

Jaa-van·2023년 5월 19일
0
post-thumbnail

Access token

=> 사용자의 권한이 확인되었을 경우 사용자를 인증하는 용도로 발급한다

jwt 도 Access token 중 하나이다

  • 서버가 무상태 (stateless) 일 때 jwt 인증 여부는 확인할 수 있지만 본인인지 확인할 수 없다는 단점이 있다
  • token 자체에 모든 정보를 가지고 있어 탈취될 경우 피해가 크다
  • 토큰을 고의적으로 만료시킬 수 없다

Refresh token

-> 모든 정보를 관리하는 것이 아니라 특정 사용자가 access token 을 발급받을 수 있게 하기 위한 용도로 사용된다
( 토큰이 탈취당할 경우 피해를 최소화 시키기 위함 )
OTP 와 같이 인증 시간이 짧고 주기적으로 재발급하기 때문에 피해가 최소화

  • 사용자의 인증정보를 서버에서 저장소&db에 저장하여 관리한다
  • 토큰을 고의적으로 만료시킬 수 있다

검증하는 과정

예를 들어 auth-middleware 로 검증을 한다고 했을 경우 access token 과 refresh token 을 한번에 검증하는 행위는 위험할 수 있다.
( access token 과 refresh token 이 한번에 탈취당할 수 있다는 보안적인 이유 )

따라서 auth-middleware 에서는 access-token 을 검증하고 만료되었을 경우 refresh-token 을 검증하는 다른 api 를 설계해서 진행한다

그러면 refresh-token 검증 api 에서 refresh-token verify 가 유효한 경우 access-token 을 재발급한 후 기존의 api 를 다시 요청한다.
만료되었을 경우 다시 로그인을 해야하기 때문에 로그인 페이지로 이동하는 로직으로 설계하였다.

refresh-token 의 경우 userId 를 저장한 상태로 redis 에 key:value 값으로 저장되어 관리하기 용이하게 설정하였다. rtVerify api 를 수행할 때 프론트엔드에게 userId 값을 받아와 그것을 refresh-token 에 저장하는 방식으로 구현하였다.

auth-middleware

const jwt = require("jsonwebtoken");

require("dotenv").config();
const env = process.env;

const { Users } = require("../models");

module.exports = async (req, res, next) => {
  const accessToken = req.headers.accesstoken
    ? req.headers.accesstoken
    : req.cookies.accessToken;

  // access token 이 존재하지 않는 경우 로그인 페이지로 이동
  if (!accessToken) {
    throw new Error("400/Access Token이 존재하지 않습니다.");
  }

  const [authType, authToken] = (accessToken ?? "").split(" ");
  if (authType !== "Bearer" || !authToken) {
    throw new Error("419/Access Token이 유효하지 않습니다.");
  }

  const userId = validateAccessToken(authToken);
  if (!userId) {
    throw new Error("403/Access Token이 만료되었습니다.");
  }

  const user = await Users.findOne({
    where: { userId: userId },
  });

  res.locals.user = user;

  // console.log(`${userId}의 Payload 를 가진 Token이 성공적으로 인증되었습니다.`);

  next();
};

function validateAccessToken(accessToken) {
  try {
    const { userId } = jwt.verify(accessToken, `${env.SECRET_KEY}`); // JWT를 검증합니다.
    return userId;
  } catch (error) {
    return false;
  }
}

authController

( /api/users/rtVefiry )

verifyRefreshToken = async (req, res, next) => {
    try {
      const refreshToken = req.headers.refreshtoken;
      const userId = req.headers.userid;

      await this.authService.verifyRefreshToken(refreshToken);

      const newAccessToken = await this.authService.createAccessTokenById(
        userId,
      );
      console.log("accessToken 을 다시 발급하였습니다!");
      res.cookie("accessToken", `Bearer ${newAccessToken}`);
      res.status(200).json({ accessToken: `Bearer ${newAccessToken}` });
    } catch (error) {
      error.failedApi = "refreshToken 검증";
      throw error;
    }
  };
}

authService

verifyRefreshToken = async (refreshToken) => {
   const [authType, authToken] = (refreshToken ?? "").split(" ");

   if (authType !== "Bearer" || !authToken) {
     throw new Error("419/refreshToken의 형식이 일치하지 않습니다.");
   }

   const refreshTokenInfo = await this.redisClientRepository.getRefreshToken(
     authToken,
   );
   if (!refreshTokenInfo) {
     throw new Error("419/refreshToken의 정보가 서버에 존재하지 않습니다.");
   }

   try {
     jwt.verify(authToken, `${env.SECRET_KEY}`);

     return true;
   } catch (error) {
     throw new Error("419/refreshToken이 유효하지 않습니다.");
   }
 };
}

## redisClientRepository

const redis = require("redis");
const sequelize = require("sequelize");
require("dotenv").config();

class RedisClientRepository {
  constructor() {
    this.redisClient = redis.createClient({
      url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
      legacyMode: true,
    });

    this.redisConnected = false;
  }

  initialize = async () => {
    this.redisClient.on("connect", () => {
      this.redisConnected = true;
      console.info("Redis connected!");
    });

    this.redisClient.on("error", (error) => {
      console.error("Redis Client Error", error);
    });

    if (!this.redisConnected) {
      this.redisClient.connect().then();
    }
  };

  setRefreshToken = async (refreshToken, email) => {
    await this.initialize();
    await this.redisClient.v4.set(refreshToken, email);
  };

  getRefreshToken = async (refreshToken) => {
    await this.initialize();
    return await this.redisClient.v4.get(refreshToken);
  };

  deleteRefreshToken = async (refreshToken) => {
    await this.initialize();
    await this.redisClient.v4.del(refreshToken);
  };
}

module.exports = RedisClientRepository;

0개의 댓글