노드숙련강의 2-4

·2022년 12월 19일
0

study

목록 보기
12/81
post-thumbnail

Access Token, Refresh Token

01. Access Token

Access Token은 사용자의 권한이 확인(ex: 로그인) 되었을 경우 해당 사용자를 인증하는 용도로 발급하게됩니다.

Cookie로 jwt를 발급하고 설정한 Expire 기간이 지날 때 인증이 만료되게 하는것 또한 Access Token이라고 부를 수 있습니다.

사용자가 Access Token을 가지고 인증을 요청할 경우 Token을 생성할 때 사용한 비밀키(Secret Key)를 가지고 인증하기 때문에, 복잡한 설계없이 코드를 구현할 수 있고, 여러 분기를 거치지 않아도 된다는 장점이 있습니다.

Access Token의 경우 Stateless(무상태) 즉, Node.js 서버가 죽었다 살아나더라도 동일한 동작을하는 방식입니다. 즉, jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만, 처음 발급한 사용자 본인인지 확인할 수는 없습니다. 🥲

Access Token은 그 자체로도 사용자를 인증하는 모든 정보를 가지고 있습니다. 그렇기 때문에 토큰을 가지고 있는 시간이 늘어날 수록 탈취되었을 때는 피해가 더욱 커지게 됩니다.

만약 토큰이 탈취되었다고 인지하더라도 저희들은 해당 토큰이 탈취된 토큰인지 알 수 없고, 고의적으로 만료를 시킬 수도 없을 것 입니다. 그러므로 저희들은 언제든지 사용자의 토큰이 탈취될 수 있다고 생각을 하고, 피해를 최소화 할 수 있는 방향으로 개발을 진행해야합니다.😊

02. Refresh Token

Refresh Token은 Access Token 처럼 해당하는 사용자의 모든 인증 정보를 관리하는 것이 아닌, 특정한 사용자가 Access Token을 발급받을 수 있게 하기 위한 용도로만 사용됩니다.

Refesh Token은 사용자의 인증정보를 사용자가 가지고 있는 것이 아닌, 서버에서 해당 사용자의 정보를 저장소 또는 별도의 DB(빠르게 접근가능한-NoSQL)에 저장하여 관리합니다. 그렇기 때문에, 서버에서 특정 Token 만료가 필요할 경우 저장된 Token을 제거하여 사용자의 인증 여부를 언제든지 제어가 가능하다는 장점이 있습니다.

그렇다면 어째서 바로 Access Token을 발급하지 않고, Refresh Token을 거쳐서 Access Token을 발급하는것일까요? 사용자에게 발급한 Token이 탈취당할 경우 피해를 최소화 하기 위해서 사용합니다.

저희가 실제 세계에서 사용하는 OTP와 같이 짧은 시간 내에서만 인증 정보를 사용할 수 있게하고, 주기적으로 재발급하여, 토큰이 유출되더라도 오랜 기간동안 피해를 입는것이 아닌, 짧은 기간동안만 사용가능하도록 하여 피해를 최소화할 수 있게 됩니다.

언제든지 토큰이 탈취될 수 있다는 것을 가정하고, 탈취를 막는것이 어렵다면, 우리는 탈취된 토큰자체를 사용할 수 있는 기간을 줄여서 피해를 막을 것 입니다. 😊

03. Refresh Token Project의 템플릿

Refresh Token은 최초로 발급받고, 짧은 만료시간을 가지는 Access Token을 발급받기 위한 수단

Refresh Token Project의 API 목록

Refresh Token Project의 API 목록

npm Package 설치

npm init -y
npm install express jsonwebtoken cookie-parser -S

Refresh Token Project - app.js

// app.js

const jwt = require("jsonwebtoken");
const cookieParser = require("cookie-parser");
const express = require("express");
const app = express();
const port = 3002;
const SECRET_KEY = `HangHae99`;

app.use(cookieParser());

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

app.listen(port, () => {
  console.log(port, '포트로 서버가 열렸어요!');
})

04. Refresh Token & Access Token 발급 API

// Refresh Token/ Access Token을 발급 API---------------------------------------------------
let tokenObject = {}; // Refresh Token을 저장할 Object

app.get("/set-token/:id", (req, res) => {
  const id = req.params.id;
  const accessToken = createAccessToken(id);
  const refreshToken = createRefreshToken();

  //tokenObject: refresh token이 어떤 사용자인지 확인하기 위해서 만든 전역객체
  tokenObject[refreshToken] = id; // Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
  res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
  res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.

  return res.status(200).send({ "message": "Token이 정상적으로 발급되었습니다." });
})

// Access Token을 생성합니다.
function createAccessToken(id) {
  const accessToken = jwt.sign(
    { id: id }, // JWT 데이터
    SECRET_KEY, // 비밀키
    { expiresIn: '10s' }) // Access Token이 10초 뒤에 만료되도록 설정합니다.

  return accessToken;
}

// Refresh Token을 생성합니다.
function createRefreshToken() {
  const refreshToken = jwt.sign(
    {}, // JWT 데이터
    SECRET_KEY, // 비밀키
    { expiresIn: '7d' }) // Refresh Token이 7일 뒤에 만료되도록 설정합니다.

  return refreshToken;
}

createAccessToken는 Access Token을 생성하는 함수!
jwt 안에는 set-token API를 호출할 때 받은 id 변수를 삽입하는데요, 해당 사용자의 id가 무엇인지 확인할 때에는 Access Token에 있는 데이터를 바탕으로 인증을 진행합니다!

createRefreshToken는 Refresh Token을 생성하는 함수!
jwt 안에는 아무런 데이터가 존재하지 않는데요, 해당하는 Refresh Token에 대한 정보는 서버에서 tokenObject라는 변수안에 할당하게 됩니다.
만약 서버에서 해당하는 Refresh Token에 대한 정보를 가지고 있지 않으면, Token의 인증은 실패하게 됨!

05. Refresh Token & Access Token 검증 API

// Refresh Token과 Access Token을 검증하는 API-------------------------------------------------------------------------
app.get("/get-token", (req, res) => {
  const accessToken = req.cookies.accessToken;
  const refreshToken = req.cookies.refreshToken;

  //토큰이 존재하지 않는 경우 걸러주기
  if (!refreshToken)
    return res
      .status(400)
      .json({ message: "Refresh Token이 존재하지 않습니다." });
  if (!accessToken)
    return res
      .status(400)
      .json({ message: "Access Token이 존재하지 않습니다." });

  //토큰 유효성 검증
  const isAccessTokenValidate = validateAccessToken(accessToken);
  const isRefreshTokenValidate = validateRefreshToken(refreshToken);

  //refreshToken 검증이 실패했을때
  if (!isRefreshTokenValidate)
    return res.status(419).json({ message: "Refresh Token이 만료되었습니다." });

  //accessToken 검증이 실패했을때 앞에서 id값을 지정받은 것을 사용
  if (!isAccessTokenValidate) {
    const accessTokenId = tokenObject[refreshToken];
    //임의로 토큰을 만료시켰을때
    if (!accessTokenId)
      return res
        .status(419)
        .json({ message: "Refresh Token의 정보가 서버에 존재하지 않습니다." });

    // 새로 받아온 id를 이용해 새 토큰생성
    const newAccessToken = createAccessToken(accessTokenId);
    res.cookie("accessToken", newAccessToken);
    return res.json({ message: "Access Token을 새롭게 발급하였습니다." });
  }

  //모든 검증이 완료된 토큰의 id
  const { id } = getAccessTokenPayload(accessToken);
  return res.json({
    message: `${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.`,
  });
});

// Access Token을 검증합니다.
function validateAccessToken(accessToken) {
  try {
    jwt.verify(accessToken, SECRET_KEY); // JWT를 검증합니다.
    return true;
  } catch (error) {
    return false;
  }
}

// Refresh Token을 검증합니다.
function validateRefreshToken(refreshToken) {
  try {
    jwt.verify(refreshToken, SECRET_KEY); // JWT를 검증합니다.
    return true;
  } catch (error) {
    return false;
  }
}

// Access Token의 Payload를 가져옵니다.
function getAccessTokenPayload(accessToken) {
  try {
    const payload = jwt.verify(accessToken, SECRET_KEY); // JWT에서 Payload를 가져옵니다.
    return payload;
  } catch (error) {
    return null;
  }
}

validateAccessTokenvalidateRefreshToken은 사용자가 전달한 Token이 정상적인 토큰인지 확인하는 함수에요!

  • Access Token 또는 Refresh Token이 저희가 발급한 토큰이 맞는지 검증합니다.
  • Access Token 또는 Refresh Token의 만료 여부를 검증합니다.

GET /get-token API 에서는 어떤 에러들이 발생?
1. 사용자가 Cookie를 전달할 때, Access Token이 없다면 에러가 발생합니다.
- { "message": "Access Token이 존재하지 않습니다." }
2. 사용자가 Cookie를 전달할 때, Refresh Token이 없다면 에러가 발생합니다.
- { "message": "Refresh Token이 존재하지 않습니다." }
3. 사용자가 전달한 Refresh Token이 인증되지 않았다면 에러가 발생합니다.
- { "message": "Refresh Token이 만료되었습니다." }
4. Refresh Token이 인증되었지만, 서버에 존재하지 않을 때 에러가 발생합니다.
- { "message": "Refresh Token의 정보가 서버에 존재하지 않습니다." }

06. 어떤 인증 방식을 사용?

개발을 진행하다보면 모든 것에서 강점을 가지는 기술은 존재하지 않는다는것을 느낄 수 있습니다. 그렇기 때문에 새로운 기술을 도입할 때는 현재 상황에 가장 알맞는 최선의 선택을 하는것이 중요합니다.

Access Token과 Refresh Token을 비교했을 때, 프로젝트를 빠르게 구현해야하거나 사용자의 요청에 대한 인증을 최소화 하기 위해서는 Access Token을 사용할 것이고, 보안성을 중요시 여기고, 서버를 좀더 탄탄하게 구성해야 할 경우에는 Refesh Token을 사용할 것입니다.

07. Quiz

jwt쿠키 API 개발하기

  • 요청사항
    1. POST /set-key 를 호출했을 때, Request로 들어온 정보를 jwt 쿠키로 만들어 할당해주세요!
    2. GET /get-key 를 호출했을 때, 등록된 jwt 쿠키를 복호화해서 반환하는 API를 만들어주세요!
    3. SecretKey는 sparta로 설정해주세요!
    4. cookie-parser 라이브러리를 사용해서 구현해주세요!
    5. Thunder Client로 테스트 해주세요!
const jwt = require("jsonwebtoken");
const cookieParser = require("cookie-parser");
const express = require("express");
const app = express();

app.use(cookieParser());
// 추가해줘야 에러가 안뜸-
app.use(express.json());
//app.use(express.urlencoded({ extended: false }));

// 쿠키할당------------------------------------------------
app.post("/set-key", (req, res) => {
  const { key } = req.body;
  const token = jwt.sign({ key: key }, "sparta", { expiresIn: "10m" });
  res.cookie("token", token);

  return res.status(200).end();
});
// 등록된 쿠키를 복호화해서 반환-------------------------------------
app.get("/get-key", (req, res) => {
  const { token } = req.cookies;
  const { key } = jwt.decode(token);
  return res.status(200).json({ key });
});

// 서버확인--------------------------------------------------------------
app.listen(5002, () => {
  console.log("5002 포트로 서버가 열렸어요!");
});
profile
개발자 꿈나무

0개의 댓글