Header:{"alg":"HS256","typ":"JWT"} — algorithm + type. Payload:{"sub":"u_42","exp":1760000000} — claim (subject + expiry). Signature: 발급자 secret/private key 쓴 base64(header) + "." + base64(payload) 의 HMAC-SHA256 이나 RSA signature.
누구든 JWT 복사, 첫 두 부분 base64-decode, claim 읽을 수 있어. Signature 가 signing key 가진 누군가가 token 발급했고 변경 안 됐다 증명. JWT 는 서명, 암호화 아냐. Payload 에 secret 넣는 게 top 5 실수.
표준 claim (왜 쓰나)
RFC 7519 가 3-글자 claim 이름 한 줌 예약:
iss (issuer) — token mint 한 사람. Client 가 맞는 signing key 신뢰.
sub (subject) — token 표현하는 누구/뭐 (보통 user ID).
aud (audience) — token 의 대상. 한 issuer 가 여러 service cross-contamination 없이 서빙.
exp (expiry) — Unix timestamp, 그 후 token 무효. 항상 포함.
nbf (not before) — Unix timestamp, 그 전 token 무효. 드문; 예약 접근에 유용.
iat (issued at) — mint Unix timestamp. Audit + grace window 에 유용.
jti (JWT ID) — unique ID. Revocation 지원 원하면 필수 (jti 로 blacklist).
자기 claim (role, scope, org_id) 자유 추가. 작게 유지 — 모든 JWT 가 모든 request 에 보냄.
핵심 보안 gotcha 셋
1. alg=none. 초기 JWT 라이브러리가 {"alg":"none"} token 받음 — signature 불필요. 공격자가 어느 token 이든 만들고 라이브러리가 받아들임. Verifier 에서 항상 alg: none 거부; 현대 라이브러리가 기본으로 함, 근데 네 것 검증.
2. Algorithm confusion. 공격자가 RS256 (비대칭) 으로 서명된 token 받고 verifier 한테 HS256 (대칭) 이라 말함. 코드가 algorithm 으로 verification key lookup 하면 verifier 가 공개 key 를 HMAC key 로 씀 — 공격자가 이미 가진 — 그리고 위조 token 받아들임. 항상 기대 algorithm 핀.
3. Payload 에 secret 넣기. Payload 는 base64-encoded, 암호화 아님. Token 가진 누구든 읽음. Password, 내부 user 디테일, 새는 token 이 노출 안 했으면 하는 거 넣지 마.
HS256 vs RS256 — 어느 algorithm
HS256 (HMAC-SHA256, 대칭): 공유 secret 하나. Issuer 와 verifier 가 같은 당사자 (혹은 완전 신뢰). 단순; 빠름; single-service backend 에 좋음.
RS256 (RSA, 비대칭): Private key 서명; public key 검증. Issuer (auth server) 가 private key 보유; verifier (여러 resource server) 가 public key 만. Issuer ≠ verifier 거나 verification 널리 배포할 때.
대부분 identity provider (Auth0, Okta, Google, Apple) 가 RS256 씀. Single-service 앱이 자주 단순함 위해 HS256.
JWT 가 verification 을 stateless 로 만듦; 그게 승리이자 거래. Request 당 DB lookup 없음 — signature 가 token 유효 증명; exp claim 이 여전히 신선 증명. 거래: stateful revocation infrastructure (Redis 의 jti blacklist) 추가 없이 exp 전에 JWT revoke 못 함. 짧은 exp + refresh token 이 실용적 타협.
Refresh 춤
짧은 access token (~15분) 이 누설 윈도우 빡빡 유지. 근데 user 한테 15분 마다 login 강제는 적대적. 타협: access token 을 더 긴 refresh token (~30일) 과 쌍:
Login 이 둘 다 발급: access (15m) + refresh (30d).
Client 가 모든 API 호출에 access 씀. 만료 가까우면 client 가 refresh token 을 /auth/refresh 에 POST, 새 access (그리고 optionally 새 refresh 로 rotate) 받음.
Refresh token 이 access token 보다 더 조심스럽게 저장 — 보통 HttpOnly cookie, 절대 localStorage 안, 일반 API 호출에 절대 안 보냄.
Logout 이 server 쪽 refresh token revoke; access token 이 15분 안에 자연 expire.
Refresh rotation 패턴: 성공한 /refresh 마다 새 refresh token 도 발급하고 옛 거 무효화. 이게 refresh token 새면 피해 한정 — 정당 user 의 다음 refresh 가 중복 사용 감지하고 전체 세션 무효화.
cwkPippa 의 JWT 현실
cwkPippa 가 현재 JWT 안 씀 — bearer token 이 expiry 가진 SQLite 에 저장된 opaque random string, DB lookup 으로 validate. Single-backend service 한텐 JWT 보다 단순하고 revocation trivial (그냥 row 삭제). API 를 여러 service 로 나누거나 verification 을 CDN edge 한테 넘기면 JWT 의 stateless verification 매력. 한 server 한텐 stateful token 이 단순함으로 이김. 맞는 도구는 topology 에 의존, 유행에 아님.
Code
손으로 JWT decode — header 와 payload 가 secret 아님·python
# 라이브러리 없이 손으로 JWT decode
import base64, json
token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1XzQyIiwiZXhwIjoxNzYwMDAwMDAwfQ.signature'
header_b64, payload_b64, sig_b64 = token.split('.')
# Base64url 이 padding 생략하니 패딩 추가
def b64url_decode(s: str) -> bytes:
return base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
header = json.loads(b64url_decode(header_b64))
payload = json.loads(b64url_decode(payload_b64))
print(header) # {'alg': 'HS256', 'typ': 'JWT'}
print(payload) # {'sub': 'u_42', 'exp': 1760000000}
# Signature 가 유일 보호 부분 — 누구든 나머지 읽음.
FastAPI 에서 JWT auth flow 처음부터 구현: /login 이 access (HS256, 15분) + refresh (opaque, 30일) token 돌려줌; /auth/refresh 가 refresh token rotate 하고 새 access 발급; /me 가 유효 access 필요. Refresh 에 재사용 감지 추가 (같은 refresh 두 번 제시면 family revoke). 그 다음 일부러 공격 셋 시도: (1) 재서명 없이 access token payload 수정 — 401 해야, (2) alg=none token 제출 — 401 해야, (3) 한 번 refresh 에 쓴 후 refresh token 제출 — 401 해야 AND family 의 다른 refresh token 들 무효화.
Hint
발급/검증에 PyJWT: jwt.encode({...}, SECRET, algorithm='HS256') 와 jwt.decode(token, SECRET, algorithms=['HS256']). 재사용 감지는 각 refresh token 과 함께 저장된 family_id 필요; rotation 에 used=True 표시; used=True 가진 request 에 같은 family_id 가진 모든 token 찾고 삭제. 세 공격이 다 401 로 실패 — 구현이 견고 증명하는 integration test.
Progress
Progress is local-only — sign in to sync across devices.