JWT 와 토큰 회전
JWT 와 토큰 회전
JWT 는 가벼운 클레임 전달 수단이지만, 그 자체로 인증 시스템의 모든 답을 주지는 않습니다. 짧은 액세스 토큰 · 긴 리프레시 토큰 패턴 · 무효화 전략 · 알고리즘 선택 · 잘 알려진 함정 (alg:none) 을 정리합니다.
1. JWT 에 대한 이야기
JWT (JSON Web Token, RFC 7519, 2015) 는 세 부분:
header . payload . signature
- header — 알고리즘 · 토큰 타입 (
{"alg":"HS256","typ":"JWT"}). - payload — 클레임 (
sub·exp·iat·iss·aud· 사용자 정의). - signature — 헤더 + 페이로드를 서명. HMAC 또는 비대칭 서명.
각 부분은 base64url 인코딩, 서명은 base64url 디코딩된 값이 아닌 인코딩된 문자열을 입력으로 합니다.
표준 클레임:
| 클레임 | 의미 |
|---|---|
iss |
발급자 |
sub |
주체 (사용자 ID) |
aud |
수신자 |
exp |
만료 시각 (Unix epoch) |
nbf |
not-before |
iat |
발급 시각 |
jti |
토큰 고유 ID |
2. 알고리즘
| 알고리즘 | 종류 | 메모 |
|---|---|---|
| HS256 / HS384 / HS512 | HMAC + SHA-2 | 대칭 키. 발급자 · 검증자가 같은 비밀 공유. |
| RS256 / RS384 / RS512 | RSA + SHA-2 | 비대칭. 비공개 키로 서명, 공개 키로 검증. 길이 권장 2048+. |
| ES256 / ES384 / ES512 | ECDSA | 비대칭. 작은 서명 크기. 곡선마다 매개변수가 다름. |
| EdDSA (Ed25519) | Edwards 곡선 서명 | 빠르고 결정적. RFC 8037. |
선택 기준:
- 서비스 내부 한 곳에서 발급 · 검증 — HS256 으로 충분한 자리가 많음.
- 다른 서비스가 검증해야 함 (분산) — 비대칭 (RSA · EC · Ed25519). 검증자에게 공개 키만 배포.
- OIDC IdP 통합 — 보통 RS256 또는 ES256 가 표준.
3. Access · Refresh 분리
JWT 만으로 세션 관리를 다루면 두 요구가 충돌:
- 장기 세션 — 사용자가 매번 로그인하지 않아도 되는 자리.
- 짧은 만료 — 토큰 탈취 시 영향 범위 최소화.
해법으로 access · refresh 토큰 분리:
- Access Token — 짧은 TTL (5~15 분). 자원 호출에 동반. 만료 후 폐기.
- Refresh Token — 긴 TTL (7~30 일). access 만료 시 새 access 발급용. 보관 위치 (httpOnly 쿠키 · 안전한 저장소) 가 중요.
4. Refresh Token Rotation
리프레시 토큰을 한 번 쓰면 새 리프레시 토큰을 받고 옛 것은 무효화. 탈취된 토큰이 한 번 쓰이는 순간 옛 사용자의 다음 회전이 실패하면서 사고를 감지할 수 있음.
서버는 refresh 토큰의 jti 와 사용자 ID 를 Redis 또는 DB 에 기록:
on refresh:
if redis.get("rt:" + jti) != "valid":
revoke all sessions for user
return 401
redis.del("rt:" + jti)
new_rt = issue_new()
redis.set("rt:" + new_rt.jti, "valid", ttl=30d)
return new_access, new_rt
옛 jti 를 다시 보면 "재사용 감지" 로 간주해 그 사용자의 모든 세션을 끊습니다.
5. 무효화 전략
JWT 의 무상태성은 강점이지만 무효화가 어려운 자리.
Block-list (Deny-list) — 무효화된 토큰의 jti 를 짧은 TTL 로 Redis 에 기록. 검증 단계에서 block-list 확인. 액세스 토큰의 TTL 만큼만 보관.
key = "bl:jwt:" + jti
ttl = exp - now + clock_skew
장점 — 단순. 한계 — 호출마다 외부 조회.
Allow-list — 발급된 모든 토큰을 추적하고 매 호출 시 확인. 강한 통제, 그러나 무상태 JWT 의 의미가 약해짐. 사실상 세션 토큰에 가까워짐.
Short TTL + Refresh Rotation — 가장 흔한 절충. 액세스 토큰 자체에는 무효화 메커니즘 없이 짧은 TTL 로 위험 창 축소, 리프레시는 회전 · 재사용 감지로.
Clock skew — exp 검증 시 서버 · 발급자의 시계 차이 흡수. 보통 30~60 초 마진. 너무 크면 만료 후 사용 가능 창이 길어짐.
6. alg:none 과 BCP
JWT 명세는 alg:none 을 정의하지만, 이 값은 검증 우회의 출발점 (JWT none-알고리즘 취약점, 2015 년 보고). 라이브러리가 기본적으로 alg:none 을 거부해야 한다는 권고가 있고, RFC 8725 (JSON Web Token Best Current Practices, 2020) 에 정리.
핵심 권고:
alg화이트리스트 명시 (['HS256','RS256']) — 라이브러리가 토큰의 헤더에서 알고리즘을 그대로 받지 않게.- 키 혼동 공격 차단 — HS256 검증 함수에 RS256 공개 키를 전달하는 사고 방지.
kid검증 — 키 ID 를 그대로 파일 경로로 쓰지 말 것.- 인증 · 암호화의 분리 (JWS · JWE).
7. 토큰 저장 위치
- httpOnly Secure 쿠키 — XSS 에서 보호. CSRF 대비 필요 (SameSite + 토큰).
- JS 접근 가능 저장소 (localStorage) — XSS 발생 시 토큰 노출. 권장도가 낮음.
- 메모리 (앱 인스턴스 전역 변수) — 새 탭 · 새로고침에 사라짐.
웹은 httpOnly 쿠키 + SameSite=Lax 또는 Strict + 짧은 access TTL 이 자주.
8. 키 관리
- 키 회전 (rotation) 일정 — JWKS endpoint 의 다중 키.
- 발급 시
kid헤더에 키 식별자. - 검증 시 JWKS 캐시 · 만료 처리.
비대칭 키의 비공개 키는 시크릿 매니저 (KMS · Vault · 1Password) 로.
9. 클레임 설계
최소 정보만 — 이메일 · 민감 정보를 페이로드에 넣지 않음 (JWT 는 서명만, 암호화 안 됨).
권한 스냅샷 — 토큰에 권한을 넣으면 사용자 권한 변경이 즉시 반영 안 됨. 짧은 TTL 로 보완 또는 권한 체크를 매번 DB 에서.
10. 자주 걸리는 자리
암호화 안 된 사실 — JWT 는 서명만. 페이로드는 누구나 디코딩해 봄. 민감 정보는 JWE 또는 서버 세션.
alg 헤더 신뢰 — 토큰이 자기 알고리즘을 선언하게 두면 공격 표면. 화이트리스트 강제.
HS256 비밀의 약함 — 짧은 비밀은 무차별 공격에 약함. 32 바이트 이상의 무작위 값.
exp 검증 누락 — 라이브러리 기본을 신뢰하지 말고 단위 테스트.
리프레시의 단일 사용 미보장 — 회전 패턴의 의미가 사라짐. 사용 시 즉시 무효화.
로그아웃의 의미 — access TTL 동안은 토큰이 살아 있음. UX 와 안전 사이의 결정.
다중 디바이스 무효화 — 한 디바이스의 비밀번호 변경이 다른 디바이스의 세션을 끊을지 정책 결정.
하고픈 말
JWT 는 무상태 인증의 가벼운 답이지만, 무효화 · 리프레시 · 키 회전이라는 셋의 운영 부담이 따라옵니다. 짧은 access TTL + Refresh Token Rotation + alg 화이트리스트 셋이 함께 있을 때 운영 안전성이 크게 좋아집니다. 민감 정보는 페이로드가 아니라 서버 세션으로.
Next
- oauth-state-pkce
- rate-limit-redis
RFC 7519 JWT · RFC 8725 JWT BCP · RFC 7515 JWS · RFC 7516 JWE · OWASP JWT Cheat Sheet · Auth0 Refresh Token Rotation · JWT.io · RFC 9700 OAuth 2.0 BCP 을 참고합니다.