JWT 가이드

JWT가 만료됐을 때 제가 복구 흐름을 점검하는 방법

expired JWT를 임의로 수정하지 않고 access token, refresh token, 재인증, clock skew, stale token cache를 순서대로 확인하는 실무 복구 가이드입니다.

작성: JWTDecoder.tools마지막 확인 2026-09-13

편집 문체 안내: 1인칭 표현은 JWTDecoder.tools의 문서화된 검증 워크플로를 뜻하며, 게시자와 조직 저자는 JWTDecoder.tools입니다.

exp → refresh / reauth → new JWT
exp → refresh / reauth → new JWT

token expired 오류를 보면 저는 payload의 exp 값을 바꾸거나 JWT를 다시 인코딩해서 살리려고 하지 않습니다. signed JWT는 header와 payload의 정확한 bytes에 대해 서명되므로 payload를 수정하면 signature가 더 이상 맞지 않습니다.

제가 먼저 하는 일은 정말 만료된 것인지 확인하고, 그 다음 시스템이 의도한 refresh 또는 reauthentication 경로를 따라가는 것입니다.

1. 먼저 실제 만료 상태를 확인합니다

JWT Expiry Checker에서 exp, iat, nbf를 봅니다.

JWT의 exp는 Unix epoch 기준 초 단위 NumericDate입니다.

JavaScript에서는:

const expired = Date.now() >= payload.exp * 1000;

처럼 밀리초로 맞춰야 합니다.

Date.now() >= payload.exp처럼 단위를 섞으면 정상 token을 잘못 판단할 수 있습니다.

2. 만료와 clock 문제를 분리합니다

“방금 로그인했는데 즉시 expired”라면 token lifetime 자체보다 clock을 의심합니다.

제가 확인하는 항목:

  • issuer server clock
  • verifier/API server clock
  • container/VM/NTP 상태
  • iat, nbf, exp 관계
  • 허용된 clock skew가 너무 작거나 너무 큰지

Timezone 표시는 사람이 보는 시간에 영향을 주지만 epoch 비교는 timezone 자체와 무관합니다.

3. Access token인지 확인합니다

OAuth/OIDC 환경에서는 access token과 refresh token 역할이 다릅니다.

  • access token: API/resource 접근에 사용
  • refresh token: 새 access token을 얻는 데 사용할 수 있는 별도 credential

refresh token도 provider에 따라 JWT일 수도 있고 opaque value일 수도 있습니다. JWT 모양이라는 이유로 역할을 판단하지 않습니다.

4. 정상적인 refresh flow를 사용합니다

시스템이 refresh token을 지원한다면 expired access token을 “수리”하지 않고 token endpoint에 refresh 요청을 보내 새 access token을 받습니다.

개념적으로:

expired access token
      ↓
valid refresh token
      ↓
token endpoint
      ↓
new access token

refresh token도 만료·폐기·rotation될 수 있으므로 실패 시 다시 로그인해야 할 수 있습니다.

5. Refresh token을 브라우저에 어떻게 저장하는지도 봅니다

보안 모델에 따라 refresh token을 browser JavaScript에서 직접 다루지 않고 secure HttpOnly cookie나 backend-for-frontend 패턴을 사용할 수 있습니다.

저는 단순 편의를 위해 long-lived credential을 localStorage에 두는 결정을 가볍게 하지 않습니다. XSS 위협 모델과 애플리케이션 구조를 함께 봅니다.

6. 새 token을 받았는데 계속 expired라면 stale state를 찾습니다

실제 버그는 refresh가 성공했지만 요청 코드가 계속 이전 access token을 보내는 경우도 많습니다.

제가 확인하는 곳:

  • API client의 Authorization header 생성 코드
  • React/state store의 오래된 값
  • 여러 browser tab 사이 상태
  • service worker/cache
  • server-side session cache
  • retry interceptor가 old request를 재사용하는지

Network panel에서 실제 전송된 Bearer token의 exp와 새로 발급받은 token의 exp를 비교하면 빠르게 확인할 수 있습니다.

7. 401 전체 원인과 분리합니다

API가 401 Unauthorized를 반환했다고 항상 expiration 때문은 아닙니다.

다음도 같은 결과를 만들 수 있습니다.

  • signature mismatch
  • wrong issuer
  • wrong audience
  • wrong token type
  • missing scope/role
  • unknown kid / stale JWKS cache

그래서 exp가 정상이라면 JWT 401 Unauthorized 가이드의 전체 순서를 따라갑니다.

8. 만료 직전의 race condition을 봅니다

token이 아직 몇 초 남아 있을 때 client가 긴 API 요청을 시작하면 verifier가 실제로 처리하는 시점에는 만료될 수 있습니다.

시스템에 따라 client가 expiration 직전 미리 refresh하거나, 서버가 작은 clock skew를 허용할 수 있습니다.

하지만 저는 너무 큰 safety window를 두어 token lifetime 정책을 사실상 무력화하지 않습니다.

9. exp가 없는 token은 어떻게 하는가

표준상 모든 JWT가 무조건 exp를 가져야 하는 것은 아니지만, authentication token에서는 애플리케이션 정책이 expiration을 요구하는 경우가 많습니다.

저는 verifier가 요구하는 claim을 명시적으로 설정합니다. “없으니 영원히 유효”처럼 해석하지 않습니다.

10. 만료된 token을 서버에서 받아주도록 바꾸지 않습니다

디버깅 중 다음과 같은 임시 수정은 위험합니다.

ignoreExpiration = true

또는 만료 검증을 완전히 끄는 설정입니다.

테스트 환경에서 원인을 격리하는 용도로 잠깐 사용할 수는 있어도 production 해결책으로 남기지 않습니다. 만료 정책은 stolen token의 usable lifetime을 제한하는 중요한 방어층입니다.

11. Refresh token rotation도 확인합니다

일부 provider는 refresh token을 사용할 때 새 refresh token을 발급하고 이전 것을 폐기하는 rotation을 사용합니다.

이 경우 애플리케이션이 새 refresh token을 저장하지 않으면 다음 refresh부터 실패할 수 있습니다.

제가 확인하는 것은:

  • refresh response에 새 refresh token이 왔는가
  • 저장 update가 atomic하게 됐는가
  • concurrent refresh가 서로 token을 무효화하지 않는가
  • reuse detection 정책이 있는가

12. Logout/revocation과 expiration을 구분합니다

아직 exp가 미래인데 token이 거부될 수도 있습니다. server-side revocation, session termination, key revocation 같은 정책이 있다면 expiration만으로 설명되지 않습니다.

반대로 JWT가 self-contained라고 해서 logout 시 즉시 무효화가 자동으로 되는 것도 아닙니다. 시스템의 revocation model을 확인합니다.

제가 사용하는 복구 체크리스트

  1. exp가 number인지 확인합니다.
  2. 현재 시각과 초/밀리초 단위를 정확히 비교합니다.
  3. iat, nbf, system clock을 확인합니다.
  4. access token과 refresh token을 구분합니다.
  5. 지원되는 경우 정식 refresh flow로 새 access token을 얻습니다.
  6. refresh 실패 시 reauthentication으로 돌아갑니다.
  7. 새 token이 실제 요청에 사용되는지 network에서 확인합니다.
  8. 반복 401이면 signature/issuer/audience/scope까지 확장합니다.
  9. expiration 검증을 끄지 않습니다.
  10. production token을 수정하거나 재서명 없이 재사용하려고 하지 않습니다.

JWT 만료 문제에서 제가 가장 중요하게 보는 것은 **“만료된 token을 어떻게 억지로 살릴까”가 아니라 “애플리케이션이 설계한 credential lifecycle이 정상적으로 작동하는가”**입니다. expiration은 오류가 아니라 보안 수명 주기의 일부이며, 올바른 해결책은 refresh·재인증·상태 동기화 흐름을 정확히 구현하는 것입니다.

출처 및 참고자료

관련 가이드