JWT 토큰

jin·2025년 10월 22일

JWT: 헤더, 페이로드, 서명 세 가지 정보를 base64로 인코딩한 값을 콤마를 사이에 두고 이어붙은 형태

Access Token

JWT는 Stateless한 방식이기 때문에 서버측에서는 이 토큰을 갖고 있는 클라이언트가 정말 클라이언트 본체인지 확인할 수가 없다. 이에 리프레시 토큰을 활용할 수 있다. 리프레쉬 토큰은 사용자 인증이 아닌 새로운 Request Access 토큰을 생성하는 용도로만 사용된다.

Access token의 유효 기간을 짧게 설정하고 Refresh Token의 유효기간을 길게 설정한다. 사용자는 두가지의 토큰을 모두 서버에 전송하는데 이때 전자가 만료되었을 경우 Refresh Token으로 새로운 Access Token을 요청할 수 있다.

만약 Access Token만 있었다면 클라이언트는 짧은 주기마다 재로그인해야 하는 번거로움 있다. 하지만 Refresh token을 통해 만료된 Access token을 갱신할 수 있다.

그런데 만일 Refresh Token을 탈취당한다면? 즉 Access Token을 언제든지(Refresh token이 만료되기 전까지) 요청할 수가 있는거다. 그러면 서버 측은 이를 확인할 방법이 없다.
이럴 경우 데이터베이스에 Access token과 Refresh token을 둘 다 저장하여 비교 검증할 수 있다.
공격자가 탈취한 Refresh token으로 새로운 Access token을 발급받아 클라이언트 인 척 요청을 하는 경우 서버는 미리 저장된 데이터 베이스의 Access token과 비교 검증을 해 볼 수 있다. 만일 아직 Access token의 만료가 되지 않았음에도 요청 시 들어온 Access token 과 데이터베이스에 저장된 Access token이 다를 경우, 이를 Refresh token이 탈취당했다고 판단하고 두 토큰을 만료시켜버린다.

그런데 이 경우, 데이터 베이스에 두개의 토큰을 모두 저장해야해서 서버 부하 측면에서 부담이다.
Refresh Token만 저장하는 방법이 있다. 마치 Refresh token을 일회용 티켓처럼 취급하는 것이다.
Refresh token의 유효 기간이 끝날 때까지 계속 재사용할 수 있다면, 탈취당했을 때 공격자는 만료 기간 내 그 토큰으로 계속 Access token을 발급할 것이고 서버는 토큰이 탈취당했는지 알 방법이 없다.

하지만 Refresh token의 일회용 티켓 규칙을 적용하면 달라진다.
1. 공격자가 RT1을 탈취해서 사용한다. 서버는 새로운 AT2,RT2를 공격자에게 발급해준다. 그리고 이제 RT2만 유효하다.
2. 정상 사용자는 AT1이 만료되었으니 기존의 RT1으로 재발급을 요청한다.
3. 서버는 RT1은 저번에 이미 사용된 티켓인데 또 요청이 들어온 것을 확인한다. 그 즉시 탈취 시도라고 인지하고 RT1,RT2을 만료시킨다.
4. 공격자는 AT2로 계속 사용자인 척 할 수 있다. AT는 Stateless한 특성이 있기 때문에 유효기간이 만료될 때까지는 서버 입장에서는 유효하기 때문이다.

바로 이게 데이터 베이스에 Refresh token만을 저장하는 일회용 티켓 규칙의 한계이다.

공격자가 발급받은 AT2를 즉시 무효화하려면, 서버가 AT2의 jti를 알고 있어야 하고, 이 jti를 블랙리스트에 추가해야 한다. 이를 위해서는 데이터베이스에 Refresh Token과 함께 해당 Refresh Token으로 발급된 Access Token의 jti를 함께 저장해야 한다.

하지만 사용자가 그 동안 refresh token을 사용하지 않는다면, 공격자는 적어도 RT2가 만료될 때까지는 계속 AT2를 사용할 수 있다. 사용자는 빨리 RT1으로 Request를 해야 서버가 RT1과 RT2가 다르다는 것을 인지할 것이다. 그 사이 시간은 어떻게 하는 것이 좋을까?

Access token과 Refresh token의 만료 시간을 설정하는 기준과 blacklist 관리하는 방법을 추가적으로 고민해보면 좋을 거 같다.

10/23

<유효 시간 관리>
Access token의 권장 유효 시간은 권장 5분~30분이라고 한다. AT는 보호된 리소스에 직접 접근하는데 사용되기 때문에, 탈취 시 피해를 최소화하기 위해 유효기간을 짧게 가져가는 것이 중요하다. 그렇다고 너무 짧으면 재발급 요청이 잦아져 서버 부하와 UX에 영향을 줄 수 있다.

Refresh token은 AT를 재발급받는 용도로 사용되기 때문에 AT보다 훨씬 긴 유효 시간을 가져야 한다. 하지만 탈취 시 위험도가 매우 높기 때문에 이를 철저히 보호해야 한다.

  • Access Token (30분):

    • 보안: 짧을수록 좋다. 만약 Access Token이 탈취되더라도, 공격자가 이를 사용할 수 있는 시간이
      30분으로 제한된다. 이는 피해를 최소화하는 데 중요하다.
    • 성능/확장성: Access Token은 Stateless하므로, 매 요청마다 DB나 Redis를 조회할 필요가 없어 서버 부하를
      줄인다. 30분은 너무 짧아서 재발급 요청이 빈번해지는 것을 막으면서도, 보안성을 유지하는 적절한
      시간이다.
    • 사용자 경험: 대부분의 사용자는 30분 내에 웹 페이지에서 작업을 완료한다. 이 시간 동안 토큰
      재발급으로 인한 불편함 없이 서비스를 이용할 수 있다.
  • Refresh Token (7일):

    • 보안: Access Token보다 훨씬 길지만, HttpOnly 쿠키, Redis 저장, 일회용 회전 및 재사용 탐지 로직으로
      철저히 보호된다. 탈취 시 위험도가 높지만, 이러한 보호 장치들이 위험을 크게 줄여준다.
    • 사용자 경험: 사용자가 매일 로그인해야 하는 번거로움을 줄여준다. 7일은 한 번 로그인하면 일주일
      정도는 편하게 서비스를 이용할 수 있는 합리적인 기간이다. '로그인 유지' 기능 등을 구현할 때는 30일
      이상으로 늘리기도 한다.

<블랙리스트 관리>

  • Refresh Token 재사용이 탐지되었을 때
  • 사용자가 명시적으로 로그아웃했을 때

단, 비밀번호 변경시에는 Refresh Token을 redis에서 삭제하여 이를 만료시킨다. 하지만 Access token 자체는 블랙리스트에 추가하지 않고 access token의 짧은 유효기간이 만료될 때까지 유효하게 둔다.

<방어 질문>

Q) 데이터 베이스에 Refresh token과 jti를 함께 저장하면 부담이 되지 않을까요?
A) 네 맞습니다. 로그인할 때 Refresh Token 정보(RT + JTI)를 Redis에 저장하는 쓰기 연산추가, 토큰 재발급시 redis에서 정보를 읽고 새로운 정보로 업데이트 하는 읽기/쓰기 연산 추가, 로그아웃시 redis에서 토큰을 삭제하는 연산 추가 등 이러한 연산들이 redis에 부하를 주고 네트워크 지연을 유발할 수 있습니다.

하지만 그에 상승하는 보안적 이점이 있습니다.
1. 즉각적인 Access Token 무효화:

  • 로그아웃 시: 사용자가 로그아웃하면 해당 Access Token의 jti를 블랙리스트에 추가하여, 유효기간이 남아있더라도 즉시 무효화할 수 있습니다. (Stateless JWT의 단점 보완)
  • 비밀번호 변경 시: 모든 세션의 Refresh Token을 무효화하고, 필요하다면 현재 활성 Access Token까지 블랙리스트에 추가하여 즉시 무효화할 수 있습니다.
  • 관리자에 의한 강제 세션 종료 시: 특정 사용자의 세션을 즉시 종료할 수 있습니다.
  1. 강력한 Refresh Token 탈취 방어 (재사용 탐지): Refresh Token이 탈취되어 공격자가 이를 사용하더라도, 정상 사용자가 다음 재발급을 시도하는 순간 서버가 이를 감지하고 모든 세션을 무효화할 수 있습니다. 이는 공격자가 탈취한 Refresh Token을 장기간 사용하는 것을 막아줍니다.
    공격자가 발급받은 Access Token까지 즉시 블랙리스트에 추가하여 '그 사이 시간'의 위험을 최소화합니다. 세션 관리의 유연성: 사용자별 세션 상태를 Redis에서 관리함으로써, 더 세밀한 세션 제어(예: 특정 기기에서만 로그인 허용)가 가능해집니다

보안은 소읽고 외양간 고치는 격이기 때문에 미리 준비하는 수 밖에 없음!!

끝 ---

profile
성장중

0개의 댓글