백엔드 개발자(node.js)가 되는 과정

[nodejs 개인 프로젝트 3일 차] Access Token, Refresh Token

soopy 2023. 6. 16. 10:23
728x90

엑세스 토큰 검증과 리프레시 토큰을 활용한 엑세스 토큰 재발급

이전까지는 API 테스트 중 엑세스 토큰이 만료 되었을 때 재로그인을 통해 다시 발급받는 방식으로 테스트를 이어갔다. 하지만 추후에는 자동으로 엑세스 토큰 만료 시 자동으로 재발급 해주는 프로세스가 진행되어야 했다. 사실 실제 네이버와 같은 서비스를 이용할 때는 내가 언제 엑세스 토큰이 재발행되는지도 모르겠고, 추측하건데 특정 사이트에 로그인 후 그대로 방치한 상태에서 하루 또는 오랜 시간 지났을 때 리프레시 토큰까지도 만료되었기 때문에 재로그인을 요청받으며 로그인 창으로 강제이동하지 않았나 예상한다. 

아무튼 엑세스 토큰의 재발급은 유저가 모르는 과정을 통해 재발급되는 프로세스여야 하지 않나 라는 생각에 아래와 같이 기능을 구현했다. 아래 기능을 정리하자면 엑세스토큰 발급, 그리고 리프레시 기반 재발급을 각각 미들웨어로 구현했고, 회원 인증이 필요한 모든 요청에 대해서는 아래 두 가지 미들웨어가 반드시 거치도록 했다. 또한 미들웨어는 varification.js 파일에 작성하여 따로 관리한다. 

// 엑세스 토큰 검증을 위한 미들웨어
function verifyAccessToken(req, res, next) {
  // auth에서 access token을 획득합니다.
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.split(' ')[1]; // Bearer 제거
  if (!token) return res.status(401).send({ msg: '엑세스 토큰을 입력해 주세요.' }); // 토큰이 없다면 종료

  // access token 검증
  jwt.verify(token, process.env.ACCESS_TOKEN_KEY, (err, user) => {
    // access token이 만료된 경우 재생성하기
    if (err) {
      req.expired = true; // 다음 미들웨어에 expired 프로퍼티에 true를 담아 보낸다.
      console.error(err.name, ':', err.message);
    }
    req.user = user;
    next();
  });
}

먼저 엑세스 토큰 발급에 대한 미들웨어이다. 만약 엑세스 토큰이 만료되었거나, 값이 다를 경우 만료된 것으로 인정한다.
코드에서 보는 것과 같이 verify 메서드의 결과가 err 즉 검증에 실패한다면 request에 true값을 담아 다음 미들웨어에 전달된다.
그리고 나서 다음 미들웨어에서 req.expired가 true값을 가질 경우 실행되고, 그렇지 않을 경우 생략된다.

// 엑세스 토큰 만료 시 재발급을 위한 미들웨어
async function replaceAccessToken(req, res, next) {
  if (req.expired) {
    try {
      const cookies = req.cookies;
      // 쿠키가 없는 경우
      if (!cookies?.issuebombomCookie)
        return res.status(403).send({ msg: '엑세스 토큰 재발급을 위한 쿠키 없음' });

      // 쿠키가 있으면
      const refreshToken = cookies.issuebombomCookie;
      // DB에 저장된 쿠키가 있는지 확인
      const user = await User.findOne({ refreshToken });
      if (!user) return res.status(403).send({ msg: '해당 쿠키에는 등록된 리프레시 토큰이 없음' });
      // 쿠키 검증
      jwt.verify(refreshToken, process.env.REFRESH_TOKEN_KEY, (err, user) => {
        // refresh token이 만료된 경우 재로그인 안내
        if (err) return res.status(403).send({ msg: '리프레시 토큰이 만료됨 (재 로그인 필요)' });

        // 신규 토큰 생성
        const accessToken = jwt.sign(
          { username: user.username, _id: user._id },
          process.env.ACCESS_TOKEN_KEY,
          { expiresIn: '30m' }
        );
        // 재발급
        res.setHeader('Authorization', `Bearer ${accessToken}`);
        res.status(200).send({ msg: '엑세스 토큰이 만료되어 재발급' });
      });
    } catch (err) {
      console.error(err.name, ':', err.message);
      return res.status(500).send({ msg: `${err.message}` });
    }
  } else {
    next();
  }
}

위 과정은 엑세스 토큰 재발급 과정이다. 이전 미들웨어에서 req.expired에 true값을 받는다면 엑세스 토큰이 만료된 것으로 간주하고 재발급을 시도한다.
이 과정에서 쿠키에 저장된 리프레시 토큰을 요청하여, 쿠키가 없거나, 값이 다를 경우에 대한 대처를 하고, 이후 리프레시 토큰마저 만료되었다면 재 로그인 요청 메시지를 남기고 종료된다.
재발급이 완료되면 headers의 authorization으로 엑세스 토큰을 보내준다.

결과적으로 아래와 같이 두 미들웨어를 붙여준다.

// 수정페이지에서 '수정하기' 클릭
postsRouter.put('/:postId', verifyAccessToken, replaceAccessToken, postsController.editPosts);

현재 엑세스 토큰을 어디에 어떤 수단으로 저장할지에 대해서는 결정하지 못했다. 해당 토큰을 리프레시와 동일하게 쿠키에 저장시킬 수도 있겠지만 프론트 단에서 private 변수로 저장하는 것을 택하는 방법이 있다고 하여 이 부분에 대한 구현은 생략했다.

토큰 발급 시 변수에 남기지 않는 클로징 기법

// 엑세스 토큰 생성기
const getAccessToken = ((username, _id) => {
  const accessToken = jwt.sign({ username, _id }, process.env.ACCESS_TOKEN_KEY, {
    expiresIn: '30m',
  });
  return () => accessToken;
})();

getAccessToken();

토큰 생성 방법에 클로저 기법을 적용했다. 이렇게 하면 생성된 토큰값이 저장된 accessToken 변수에 접근이 불가해진다. 그러므로 중간에 가로챌 수 있는 여지를 제거한다. 라고 생각하고 만들었지만 올바르게 활용한 것인지, 이 부분이 보안에 도움이 되는지는 아직 확신이 안선다.

res.setHeader('Authorization', `Bearer ${getAccessToken(username, _id)}`);

토큰 생성 함수는 위 코드처럼 생성과 동시에 클라이언트의 Header로 보내진다. 그러므로 자바스크립트 내에서의 탈취는 방지할 수 있다고 생각한다.

728x90
728x90