exp (expiration)
이 시각 이후에는 받아들이면 안 됩니다. 현재 시각이 exp와 정확히 같으면 아직 유효합니다. 필수 클레임이 아니라서 없으면 만료가 없는 토큰이 되는데, 이건 대개 실수입니다.
브라우저 내부 처리
"토큰이 만료됐다"는 로그는 대개 만료가 아닙니다. 숫자를 잘못 읽었거나, 검증을 안 했거나, 두 서버의 시계가 다른 것입니다. 셋을 구분하는 방법을 정리했습니다.
JWT에는 시간을 다루는 클레임이 세 개 있고 셋 다 Unix epoch 초입니다. 밀리초가 아닙니다. 이 한 줄이 실무 오류의 절반을 설명합니다.
이 시각 이후에는 받아들이면 안 됩니다. 현재 시각이 exp와 정확히 같으면 아직 유효합니다. 필수 클레임이 아니라서 없으면 만료가 없는 토큰이 되는데, 이건 대개 실수입니다.
이 시각 전에는 받아들이면 안 됩니다. 미래 시점부터 유효한 토큰을 미리 발급할 때 씁니다. 자주 쓰이지 않지만, 발급 서버 시계가 조금 빠르면 방금 만든 토큰이 "아직 유효하지 않음"으로 거절되는 원인이 됩니다.
발급 시각입니다. 유효성 판단에 직접 쓰이지 않고 "토큰이 너무 오래됐는지" 같은 자체 정책이나 감사 로그에 씁니다. iat를 만료 계산에 쓰면 exp가 있는데도 이중 기준이 생겨 혼란해집니다.
값을 직접 확인하려면 JWT 디코더/검사기에 토큰을 붙여넣으세요. epoch 숫자를 사람이 읽는 시각으로 바꿀 때는 Unix 타임스탬프 변환기를 같이 씁니다.
이 구분을 놓치면 보안이 없는데 있는 것처럼 보입니다. JWT 관련 사고에서 가장 비중이 큰 항목입니다.
JWT의 헤더와 페이로드는 암호화가 아니라 Base64URL 인코딩입니다. 누구나 풀어서 내용을 볼 수 있습니다. 이 사이트의 디코더도, 브라우저 콘솔 한 줄도 할 수 있습니다. 그래서 JWT 페이로드에 비밀을 담으면 안 됩니다. 주민번호, 내부 식별자, 권한 상세를 넣어둔 토큰은 그 자체로 정보 노출입니다.
세 번째 조각인 서명을 비밀키나 공개키로 확인해야 "이 토큰이 우리가 발급한 것이고 내용이 바뀌지 않았다"가 성립합니다. 검증을 건너뛰고 페이로드만 읽어 쓰면, 공격자가 {"role":"admin"}으로 바꾼 토큰을 그대로 신뢰하게 됩니다.
많은 JWT 라이브러리가 decode()와 verify()를 둘 다 제공합니다. decode()는 서명을 확인하지 않습니다. 이름이 짧아서 자동완성으로 먼저 잡히고, 로컬 테스트에서는 잘 돌기 때문에 그대로 배포되는 경로가 흔합니다. 코드 리뷰에서 decode를 검색해보는 것만으로 걸러지는 경우가 많습니다.
디코딩이 누구나 가능하므로 로그에 남은 토큰은 그대로 쓸 수 있는 자격증명입니다. 만료 전까지는 유효합니다. 요청 헤더를 통째로 로깅하는 설정이 있으면 Authorization 헤더를 마스킹하세요.
JWT 규격에는 서명 없음을 뜻하는 none 알고리즘이 있습니다. 검증 코드가 헤더의 alg를 그대로 따르면, 공격자가 alg를 none으로 바꾸고 서명을 지운 토큰을 보내 통과시킬 수 있습니다. 오래된 라이브러리에서 실제로 났던 취약점입니다.
RS256은 개인키로 서명하고 공개키로 검증합니다. 공격자가 alg를 HS256으로 바꾸면, 검증 코드가 그 공개키를 HMAC 비밀키로 쓰게 됩니다. 공개키는 공개되어 있으므로 누구나 유효한 서명을 만들 수 있습니다.
검증할 때 기대하는 알고리즘을 코드에 못박습니다. 대부분의 라이브러리가 algorithms: ['RS256'] 같은 인자를 받습니다. 헤더가 무엇이라고 주장하든 그것만 허용합니다. 토큰의 헤더는 입력값이지 설정이 아닙니다.
발급 서버 시계가 검증 서버보다 몇 초 빠르면, 검증 시점에서 볼 때 iat와 nbf가 미래입니다. 그래서 "아직 유효하지 않음"으로 막힙니다. 컨테이너나 가상 머신에서 NTP 동기가 느슨하면 몇 초 차이는 흔합니다.
반대로 검증 서버가 빠르면 아직 유효한 토큰이 만료로 보입니다. 재현이 안 되는 간헐적 401의 흔한 원인입니다. 특정 인스턴스에서만 발생한다면 시계를 먼저 보세요.
대부분의 라이브러리가 clockTolerance 또는 leeway 옵션을 제공합니다. 30~60초 정도가 관행입니다. 이걸 몇 분으로 키우면 만료된 토큰이 그만큼 더 살아 있게 되므로 근본 해결(NTP)을 대신할 수는 없습니다.
액세스 토큰 exp를 길게 잡으면 탈취됐을 때 그만큼 오래 쓰입니다. JWT는 서버가 상태를 갖지 않으므로 발급된 토큰을 취소할 방법이 기본적으로 없습니다. 그래서 수명을 짧게 두고 리프레시 토큰으로 갱신하는 구조를 씁니다. "로그아웃했는데 토큰이 여전히 유효하다"는 문제가 여기서 나옵니다.
date 한 줄이면 됩니다.verify가 아니라 decode를 쓰고 있는지, 허용 알고리즘을 못박았는지 봅니다.이어서 볼 페이지: JWT 디코더/검사기로 클레임을 읽고, 타임스탬프 변환기로 시각을 대조하고, 페이로드가 Base64일 뿐이라는 점은 Base64 인코더/디코더로 직접 확인할 수 있습니다.
서버 시계가 어긋난 경우가 가장 흔합니다. 발급 서버가 조금 빠르면 받는 쪽 기준으로 nbf가 아직 미래입니다. 대부분의 라이브러리가 clock skew 허용값(보통 30~60초)을 제공하니 그것을 먼저 확인하세요.
JWT 표준(RFC 7519)의 시간 클레임은 초 단위입니다. Date.now()를 그대로 넣으면 1000배가 되어 사실상 만료되지 않는 토큰이 발급됩니다. 보안 사고로 이어지는 실수라 발급 코드를 먼저 확인하세요.
JWT의 헤더와 페이로드는 Base64URL 인코딩일 뿐 암호화가 아닙니다. 누구나 읽을 수 있고 내용을 바꿔 다시 인코딩할 수도 있습니다. 서명 검증을 통과해야 신뢰할 수 있으며, 페이로드에 비밀 값을 담아서는 안 됩니다.
입력
{
"sub": "user-123",
"iat": 1767225600,
"nbf": 1767225600,
"exp": 1767229200
}
결과
iat 발급 시각 2026-01-01 00:00:00 UTC
nbf 유효 시작 2026-01-01 00:00:00 UTC
exp 만료 2026-01-01 01:00:00 UTC
유효 기간 1시간
세 값 모두 초 단위 epoch입니다. 밀리초를 넣으면 수만 년 후로 해석되어 만료 검사가 무력화됩니다.
입력
현재 1767225600 · nbf 1767229200
결과
아직 사용할 수 없음 (1시간 뒤부터 유효)