JWT 가이드

JWT exp claim을 제가 정확히 계산하는 방법

JWT exp는 Unix epoch 기준 초 단위 NumericDate입니다. 만료 판정, JavaScript 밀리초 변환, clock skew, iat/nbf와의 관계를 실무적으로 설명합니다.

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

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

iat, nbf, 활성 구간, exp를 보여 주는 JWT 타임라인
만료를 원인으로 보기 전에 iat, nbf, 활성 구간, exp를 확인합니다.

JWT 만료 문제를 볼 때 저는 exp 값을 사람이 읽을 수 있는 날짜로 바꾸기 전에 단위부터 확인합니다. JWT의 exp는 RFC 7519의 NumericDate로, Unix epoch 기준 초(seconds) 입니다. 반면 JavaScript의 Date.now()는 밀리초이므로 이 차이를 놓치면 만료 판정이 완전히 틀어질 수 있습니다.

빠르게 현재 상태를 확인하려면 JWT Expiry Checker에서 exp, iat, nbf와 active window를 함께 봅니다.

exp가 의미하는 것

예를 들어 payload가 다음과 같다고 가정합니다.

{
  "iat": 1789320000,
  "exp": 1789323600
}

exp 시각 이후에는 verifier가 token을 받아들이면 안 됩니다. 이것은 “그 시간까지 무조건 안전하다”는 뜻이 아니라 시간 조건 하나를 만족한다는 뜻입니다.

signature, issuer, audience, token type과 권한은 여전히 별도 검증이 필요합니다.

JavaScript에서 가장 흔한 실수

제가 자주 보는 오류는 다음 비교입니다.

Date.now() >= payload.exp

왼쪽은 밀리초, 오른쪽은 초이므로 단위가 다릅니다.

올바른 형태는:

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

날짜로 표현할 때도:

const expiresAt = new Date(payload.exp * 1000);

처럼 * 1000을 적용합니다.

Node/Python에서도 단위를 명시적으로 봅니다

Python의 time.time()은 일반적으로 초 단위 float를 반환하므로 JWT NumericDate와 비교하기 쉽습니다.

import time
expired = time.time() >= payload["exp"]

하지만 사용하는 date/time library마다 단위가 다를 수 있으므로 저는 API 문서를 확인합니다. 숫자가 “epoch time”이라는 말만 보고 초인지 밀리초인지 추측하지 않습니다.

exp가 없으면 invalid인가

JWT 표준 자체에서 모든 JWT에 exp가 무조건 존재해야 하는 것은 아닙니다. 하지만 애플리케이션 정책은 exp를 필수로 요구할 수 있습니다.

제가 authentication access token을 검증한다면 대개 명확한 lifetime을 원합니다. 그래서 verifier 설정에서 expiration을 요구하도록 할 수 있습니다.

즉:

표준에서 optional
≠
내 애플리케이션에서도 optional

입니다.

iat, nbf, exp를 함께 봅니다

저는 시간 claim을 따로따로 보지 않고 timeline으로 봅니다.

iat -------- nbf ===== usable window ===== exp

실제 token에서는 iat와 nbf가 같은 값이거나 nbf가 없을 수도 있습니다.

iat

발급 시각입니다. token age와 lifetime을 이해하는 데 도움이 됩니다.

nbf

이 시각 전에는 token을 사용하면 안 됩니다.

exp

이 시각 이후에는 token을 받아들이면 안 됩니다.

이 세 값의 관계가 비정상적이면 issuer clock이나 발급 설정을 의심합니다.

Lifetime은 exp - iat로 봅니다

iat와 exp가 모두 있으면 token lifetime을 계산할 수 있습니다.

const lifetimeSeconds = payload.exp - payload.iat;

예를 들어:

exp - iat = 3600

이면 약 1시간 lifetime입니다.

제가 JWT Security Analyzer에서 maximum lifetime을 넣을 수 있게 한 이유도 이 값을 policy 관점에서 보는 데 도움이 되기 때문입니다.

Clock skew는 얼마나 허용해야 하는가

분산 시스템에서는 서버 시계가 완벽히 같은 순간을 가리키지 않을 수 있습니다. 그래서 verifier가 작은 leeway/clock skew를 허용할 수 있습니다.

하지만 저는 큰 skew를 넣어서 지속적인 clock 문제를 숨기지 않습니다.

예를 들어 몇 초 또는 시스템 요구에 맞는 제한된 tolerance는 합리적일 수 있지만 수분~수십 분을 무조건 허용하면 expiration policy 의미가 약해질 수 있습니다.

“방금 발급받았는데 expired”일 때

제가 확인하는 순서입니다.

  1. payload의 exp 값을 raw number로 봅니다.
  2. iat도 함께 봅니다.
  3. 두 값이 초 단위인지 확인합니다.
  4. issuer와 verifier의 system clock을 확인합니다.
  5. timezone 표시 문제와 epoch 계산 문제를 분리합니다.
  6. token이 실제로 오래된 cache에서 온 것이 아닌지 확인합니다.
  7. frontend가 이전 token을 재사용하는지 확인합니다.

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

“아직 만료 전인데 401”일 때

exp만 정상이라고 token이 accept되는 것은 아닙니다.

다음도 확인합니다.

  • signature가 맞는가
  • iss가 expected issuer인가
  • aud가 현재 API인가
  • nbf가 미래가 아닌가
  • 올바른 access token인가
  • 필요한 scope/role이 있는가

전체 401 흐름은 JWT 401 Unauthorized 가이드에서 확인할 수 있습니다.

Refresh token과 access token을 구분합니다

access token이 만료되면 시스템에 따라 refresh token 또는 재인증 flow로 새 access token을 얻습니다.

저는 expired access token의 exp 값을 client에서 임의로 바꾸거나 payload를 다시 인코딩하지 않습니다. signature가 깨지고 trust model도 무너집니다.

만료 후 복구 흐름은 JWT Expired Token 가이드에서 더 자세히 다룹니다.

exp를 client에서만 검사하면 충분한가

아닙니다. client-side expiration check는 UX 최적화에는 유용하지만 보안 경계가 아닙니다.

브라우저 clock은 사용자 환경이고 client code도 신뢰할 수 없습니다. resource server는 자신의 verifier에서 expiration을 확인해야 합니다.

제가 사용하는 모델은:

client check → 불필요한 요청 줄이기 / UX
server check → 실제 authentication enforcement

입니다.

이미 검증된 library를 우선합니다

production verifier에서는 직접 epoch 비교만 작성하기보다 JWT library가 제공하는 expiration 검증을 사용하고, clock tolerance와 required claims를 명시적으로 설정합니다.

직접 구현이 필요한 경우에도 다음 edge case를 테스트합니다.

  • exp가 현재보다 과거
  • exp가 현재와 정확히 같음
  • exp가 가까운 미래
  • exp가 문자열
  • exp가 없음
  • 지나치게 큰/이상한 값
  • nbf가 미래
  • iat가 미래

제가 사용하는 만료 디버깅 체크리스트

  1. exp가 number인지 확인합니다.
  2. NumericDate가 초 단위임을 확인합니다.
  3. JavaScript에서는 * 1000을 적용합니다.
  4. iat, nbf와 함께 timeline을 봅니다.
  5. issuer/verifier clock을 확인합니다.
  6. 작은 clock skew만 의도적으로 허용합니다.
  7. exp가 정상이어도 signature, issuer, audience를 별도로 검증합니다.
  8. server가 expiration을 실제로 강제하는지 확인합니다.
  9. expired token을 수정해서 재사용하지 않고 정식 refresh/reauth flow를 사용합니다.

제가 exp를 볼 때 가장 중요한 원칙은 숫자를 날짜처럼 보이게 만드는 것보다, 그 숫자의 단위와 verifier policy를 정확히 이해하는 것입니다. 그래야 “만료됐는데 통과”하거나 “정상 token을 만료로 오판”하는 문제를 피할 수 있습니다.

출처 및 참고자료

관련 가이드