Sequence Diagram으로 보는 토큰 만료 시나리오(with JWT 토큰, Axios Interceptors)

osdsoonhyun·2024년 1월 26일

Sel-Q, 셀큐

목록 보기
1/4
post-thumbnail

axios를 통한 api 요청 설정과 interceptors를 활용하여 jwt 토큰 관리에 대해 정리해보자

사용자 인증/인가 처리 방식 : JWT 토큰 방식

JWT는 Json Web Token의 약자로, JSON 형식의 웹에서 사용되는 토큰을 말한다. 주로 사용자의 인증/인가 처리를 위해 클라이언트와 서버 사이에서 정보를 주고받는데 사용한다.
JWT 방식 외에도 OAuth 방식세션 방식이 있다.
세션 방식에서 사용자의 인증/인가 처리는 쿠키와 세션을 사용하여 로그인하면 서버 단에 사용자의 세션을 DB나 캐시(cache)에 저장해놓고 쿠키로 넘어온 세션 ID로 사용자 데이터를 조회한다.
그러나 OAuth와 세션 방식은 대부분 서버 단에서 처리하기 때문에 간편(?)할 수 있지만, 전체적인 사용자 인증/인가 프로세스에 대해 공부하기 위해 토큰 방식을 채택하였다.

JWT 방식

사용자가 로그인을 하면 서버는 사용자의 정보를 토큰에 담아 발급한다. 클라이언트는 이 토큰을 저장해두었다가 서버에 요청을 보낼 때마다 이 토큰을 함께 보내어 사용자를 인증한다.
고수 개발자분이 서버는 집주인, 클라이언트는 손님, 토큰은 열쇠(또는 입장권)라고 비유를 들어서 설명해 주셨는데 위 비유로 설명을 이어하면, 권한이 필요한 API를 요청할 경우 손님(클라이언트)은 열쇠(토큰)를 가지고 집(서버)에 들어갈 수 있다는 것이다. 강도가 열쇠도 없이 집에 들어오는 것은 있을 수 없는 일이다.
이해가 쏙쏙 쉽게 된다!

JWT 장단점

장점

JWT는 사용자의 상태를 서버에 저장하지 않고 토큰 자체에 사용자 정보를 저장하고 서버는 클라이언트의 요청이 들어올 때마다 검사를 한다. 이를 통해 서버에서는 사용자의 정보를 저장하지 않아도 되기 때문에 서버 부담이 덜해진다.

단점

JWT는 서버에서만 유효성을 검증할 수 있지만 그 안에 저장된 데이터는 누구나 쉽게 열람이 가능하다. decoding 변환이 쉽기 때문에 XSS 탈취 위험, 민감한 유저의 정보들을 담으면 안 된다. 또한 발행된 토큰에 대해 서버는 제어할 수 없어 탈취 당했을 때 보안에 취약하다는 단점이 있다.

위와 같은 단점을 해결하기 위해 방법

  • jwt는 decoding 변환이 쉽기 때문에 탈취될 경우를 대비하여 HttpOnly Cookie를 통해 관리하면 자바스크립트로 접근이 불가능하기 때문에 탈취가 어렵다.
  • 유효 기간이 다른 2개의 토큰을 발행하는 것이다. 유효 기간이 짧은 accessToken과 유효 기간이 긴 RefreshToken 발행한다. accessToken은 제한된 기간 동안 유효하며, 유효 기간이 만료되면 더 이상 사용할 수 없다. RefreshToken accessToken 유효 기간이 만료되었을 때, 새로운 accessToken을 발급받기 위해 사용되는 토큰이다.

토큰 탈취 대응 시나리오

토큰 탈취될 경우에 대한 문제 상황을 가정해 보고 어떻게 해결할 수 있을지 간략히 알아보자!

Q . RefreshToken 탈취될 경우는 어떻게 해야 할까?

A . accessToken은 유효기간을 짧게 설정하기 때문에 탈취 당해도 상대적으로 괜찮다. 그러나 RefreshToken 경우 토큰을 계속 발행하기 때문에 탈취 시 위험하다. 이는 RefreshToken rotation 방법을 통해 RefreshToken 재발급 요청하거나 재사용하는 경우 accessToken을 발급하지 않도록 하여 1회용으로 사용하도록 한다.

Q . 한 아이디로 여러 사용자가 접근하였을 때, 토큰이 유효하면 항상 로그인될 텐데 어떻게 해결할 수 있을까?

A . Redis에 refreshToken을 저장하고 재발급 받을 때 Redis에 저장된 토큰과 갱신의 시도할 때 매칭되지 않으면 탈취 위협으로 판단하고 제거한다. 최악의 경우이지만, 사용자에게는 로그아웃된 정도이기 때문에 미미하다.
위 방법은 해커가 토큰이 탈취했을 때, 정상 사용자 보다 먼저 accessToken을 재발급 받는 경우 또한 해결해 준다.

만약 세션으로 관리하게 된다면, 바로 확인이 가능하기에 간단히 이미 접속한 적이 있는 아이디로 다시 접속할 경우를 막을 수 있다.
Redis에 저장하는 방식의 해결 방법은 아래 참고 자료를 참고하시기를!

Q . 더 나아가, 만약 사용하지 않는 refreshToken이 탈취 당하면 어떻게 해야 할까?

A . 개발자 컨퍼런스에서 위와 같은 질문에 아래와 같은 답을 들었던 기억이 있다.
어떤 방법을 사용해도 RefreshToken이 탈취 당하면 피해 받을 위험성이 존재한다. AccessToken 발급 기록을 데이터베이스에 보관하면 되지 않을까?
→ 토큰 기반 인증 방식의 이유인 stateless 하다는 특징을 이용해 확장성을 갖도록 하는 것이 장점인데 서버에 저장하면 세션과 다를 바가 없다.


Axios Interceptors: 로그인, 토큰 만료 시나리오

Sel-Q 프로젝트에서 어떻게 토큰 인증 로직을 구현했는지 시퀀스 다이어그램을 통해 시각화 해보았다.

로그인해서 토큰 받아오는 경우

axios 설정

Sel-Q 프로젝트에서 인가가 필요 없는 일반 API와 인가가 필요한 API 두 가지 경우로 나눠 axios.create로 인스턴스를 커스텀 하여 사용했다.

const defaultConfig = {
  baseURL: DEPLOY === 'development' ? DEV : PROD,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
  },
};

// 일반 API
export const api = axios.create(defaultConfig);

// 인가된 API
export const authApi = axios.create(defaultConfig);

두 가지 경우로 나눠서 인가가 필요한 경우에는 interceptors를 사용해서 request와 response를 가로채서 토큰 로직을 처리하였다.

어떻게 상황을 나눠서 interceptors로 인가 처리하는지는 알애ㅔ서 자세히 알아보도록하자!

로그인한 경우

로그인을 성공적으로 하면 서버에서 accessToken과 RefreshToken 를 발급해 준다. 위에서 설명한 것과 같이 토큰 탈취 위험이 있기에 유효기간이 다른 두 토큰을 발급받는다. accessToken은 10분, RefreshToken 2주로 설정해 두었다.

권한이 필요한 리소스에 접근할 경우

axios interceptors request

축구할 때 패스를 끊는 것을 인터셉트(intercept)라고 하는데 axios interceptors 는 요청을 보낼 때 인터셉트를 해서 요청 헤더에 토큰을 추가해 준다. 코드로 확인해보자.

// 인가된 API
export const authApi = axios.create(defaultConfig);

authApi.interceptors.request.use((config) => {
  const token = getCookie('Authentication');

  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

권한 필요한 리소스 접근할 경우 sequence diagram

요청이 전달되기 전에 토큰을 config headers에 설정을 해준다. 서버에서는 헤더에 토큰을 검증하고 유효하다면 응답으로 리소스를 받아온다.

권한이 필요한 리소스 접근 시 AccessToken 만료된 경우

axios interceptors response

interceptors의 request에서 인가가 필요한 API의 경우 헤더에 AccessToken을 넣어서 요청했다. AccessToken이 만료된 경우, response로 401 Error인 Unauthorized를 내보낸다.
여기서 응답을 인터셉트(intercept)해서 처리하는 방법이 있다. 유효기간이 긴 RefreshToken을 통해 AccessToken을 재발급 받아 다시 헤더에 설정하여 리소스를 재요청하도록 한다.

// AccessToken 만료시
authApi.interceptors.response.use(
  (response) => response,
  async (error) => {
    const config = error.config;

    if (
      error.response.statusText === 'Unauthorized' &&
      config.headers.Authorization
    ) {
      try {
        const newAccessToken = await refreshAuth();

        config.headers.Authorization = `Bearer ${newAccessToken}`;

        return authApi(config);
      } catch (error) {
        // RefreshToken 만료된 경우
        alert('세션이 만료되었습니다. 다시 로그인해 주세요.');
        // 토큰 스토리지 삭제하기
      }
    }
  }
);

권한이 필요한 리소스 접근 시 AccessToken 만료된 경우 sequence diagram

권한이 필요한 리소스에 접근할 경우와 3. AccessToken 검증까지 동일한 과정을 거치지만, AccessToken 만료를 확인하고 RefreshToken을 통해 새로운 토큰을 발급 받아 Resource를 재요청하는 과정을 거친다.

권한이 필요한 리소스 접근 시 RefreshToken 만료된 경우

권한이 필요한 리소스 접근 시 RefreshToken 만료된 경우 sequence diagram

권한이 필요한 리소스 접근 시 AccessToken 만료된 경우에서 7. RefreshToken 검증까진 동일하지만 RefreshToken이 만료된 것을 확인하고 에러 응답을 내보내면 이후에는 Client에서는 기존의 토큰을 모두 삭제하고 사용자에게 재로그인하도록 한다.

여기서 주의할 점은 에러를 응답받을 때, 계속해서 요청이 들어가지 않도록 해야 한다. 이렇게 하지 않으면 무한으로 요청이 들어가기 때문이다.

여기서 나는 의문점이 들었다. 매번 헤더에 토큰을 넣어주고 서버는 그것을 확인하는 일련의 과정들에서 비용이 낭비된다고 느껴졌기 때문이다.
고수분이 이에 대해 답을 주셨는데, 물론 여러 다른 보안 이슈들이 있겠지만 '탭을 여러 개 띄우고 있을 수 있다' 라고 하였고 바로 고개가 끄덕여졌다.

회고

성과

axios interceptors 를 활용하기 전에는 Resource 요청 시 일일이 어떤 API 요청이 권한이 필요한지 체크를 하며 token을 헤더에 추가해서 요청을 하곤 했다. 그뿐만 아니라 토큰이 만료되고 갱신하기 위한 로직까지 처리하려면 이것은 매우 번거로웠고 유지 보수하기 굉장히 어려웠다.

위 문제를 해결하기 위해, 일반 API인가가 필요한 API 두 가지 경우로 나눠 axios.create로 인스턴스를 커스텀하여 리팩토링을 하여 코드의 반복을 줄이고 재사용성을 높였다. 또한 토큰을 interceptors를 활용하여 헤더에 자동으로 추가하고 토큰 만료 시 자동으로 갱신하도록 구현하여 가독성과 유지 보수성을 높일 수 있었다.

개선점

기존에 있던 프로젝트를 리팩토링하다보니 서버 쪽의 개선이 불가능한 점이 아쉬웠다.
전에 지식이 부족해서 쿠키로 내려주는 것이 안전하다고 하길래 쿠키로 내려받았다. HttpOnly Cookie를 통해 관리하면 자바스크립트로 을 막아놔야 안전한 건데, 이를 개선하지 못한 점이 아쉬웠다.
또한 지금은 서버에서 AccessToken과 RefreshToken 둘 다 내려받는데, 어찌 됐던 RefreshToken 탈취될 위험이 있기 때문에 다음번에 사용자 인증/인가 처리를 한다면 로그인 시 AccessToken만 내려받고 RefreshToken 서버에서 처리하도록 하면 보안 측면에서 더 좋을 것 같다.

참고자료

Axios 공식문서
JWT-Json Web Token
JWT는 어디에 저장해야할까? - localStorage vs cookie
Redis를 이용한 토큰 탈취 대응 시나리오

0개의 댓글