실제로 실행하며 배우는 JWT 인증 (설정 불필요)
요약
본 문서는 JWT(JSON Web Token)의 구조와 작동 원리를 설명하며, 특히 클라이언트가 브라우저 기반 라이브 샌드박스를 통해 직접 토큰을 디코딩하고 취약점을 악용하는 실습 위주로 구성되어 있습니다. JWT는 서명된 자가 수용적 문자열이며, 서버는 세션 조회 없이도 서명을 검증하여 사용자 정보를 신뢰할 수 있습니다. JWT의 핵심 구조(Header.Payload.Signature)와 표준 클레임(iss, sub, exp 등)을 설명하고, 서버가 토큰을 검증하는 9단계 체크리스트를 제시합니다. 특히 'alg:none' 취약점 악용과 같은 실제 공격 시나리오를 다루며 개발자가 반드시 테스트해야 할 보안 포인트를 강조합니다.
핵심 포인트
- JWT는 서명된 자가 수용적(self-contained) 문자열로, 서버는 세션 조회 없이도 토큰 자체의 서명을 검증하여 신뢰성을 확보합니다.
- JWT는 Header.Payload.Signature 세 부분으로 구성되며, Payload는 암호화되지 않고 오직 서명만 되어 있습니다.
- 서버가 JWT를 검증할 때는 alg 일치 확인, 비밀키를 사용한 서명 재계산 및 비교 등 9단계의 엄격한 체크리스트를 거쳐야 합니다.
- 개발자는 'alg:none' 취약점 악용과 같이 서버가 필수적인 검증 단계를 건너뛰는 경우에 대비하여 보안 테스트를 수행해야 합니다.
실제 JWT를 디코딩하고, 30초 만에 alg:none 취약점을 악용하며, 여러분의 인증 시스템에서 정확히 무엇을 테스트해야 하는지 배워보세요 — 이 모든 과정은 브라우저에서 라이브 샌드박스(sandbox)를 대상으로 진행됩니다.
대부분의 JWT 튜토리얼은 다이어그램 하나를 보여주고 끝냅니다. 이 튜토리얼은 다릅니다. 모든 예제는 브라우저 내의 실제 샌드박스 API를 대상으로 실행되므로, 토큰을 디코딩하고, alg:none을 악용하며, 서버가 여러분이 던지는 값을 실제로 거부(또는 수락)하는 과정을 직접 확인할 수 있습니다. 만약 라이브러리가 내부적으로 무엇을 하는지 100% 확신하지 못한 채 JWT 구현을 승인한 적이 있다면, 이 글은 여러분을 위한 것입니다.
JWT의 실제 정체
JWT (JSON Web Token, "jot"라고 발음)는 요청을 인증하는 데 사용되며, 사용자에 대한 클레임(claims)을 담고 있는 서명된 자가 수용적(self-contained) 문자열입니다. "자가 수용적(self-contained)"이 핵심 키워드입니다. 서버는 세션(session)을 조회할 필요가 없습니다. 토큰 자체에 사용자 정보와 더불어 변조되지 않았음을 증명하는 서명이 포함되어 있기 때문입니다. 서버는 단지 서명을 검증하고 클레임을 신뢰할 뿐입니다.
** 세 가지 부분
JWT는 점(.)으로 구분된 세 개의 Base64URL 인코딩된 세그먼트로 구성됩니다:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWxpY2UifQ.sig_bytes_here
↑ header (헤더)
↑ payload (페이로드)
↑ signature (서명)
처음 두 개를 디코딩하면 JSON이 나옵니다.
Header: { "alg" : "HS256" , "typ" : "JWT" }
Payload (claims): { "sub" : "user-123" , "email" : "alice@example.com" , "role" : "admin" , "iat" : 1712345678 , "exp" : 1712349278 }
Signature: HMAC-SHA256(header.payload, secret) — 또는 비대칭 알고리즘(asymmetric algorithms)을 위한 RSA/ECDSA 서명.
⚠️ 누구나 JWT를 디코딩할 수 있습니다. 페이로드는 암호화(encrypted)된 것이 아니라 서명(signed)된 것입니다. JWT에 비밀 정보(비밀번호, 신용카드 번호, 로그에 남으면 안 되는 모든 것)를 절대 넣지 마세요.
표준 클레임 (Standard claims, RFC 7519)
| 클레임 (Claim) | 의미 (Meaning) |
|---|---|
| iss | 발행자 (Issuer) — 토큰을 생성한 주체 |
| sub | 대상 (Subject) — 토큰의 대상 (사용자 ID) |
| aud | 수신자 (Audience) — 토큰이 누구를 위한 것인지 |
| exp | 만료 시간 (Expiration) — 토큰이 무효화되는 Unix 타임스탬프 |
| nbf | 활성 시작 시간 (Not Before) — 토큰이 무효한 Unix 타임스탬프 |
| iat | 발행 시간 (Issued At) — 토큰이 생성된 시점 |
| jti | JWT ID — 취소(revocation)를 위한 고유 식별자 |
여기에 귀하의 앱에 필요한 모든 커스텀 클레임 (custom claims)을 추가할 수 있습니다: role, tenant_id, permissions 등.
로그인을 실행하고 실제 토큰을 받아보세요
다음은 퍼블릭 샌드박스(public sandbox)를 대상으로 하는 로그인 요청입니다:
POST https://demo.totalshiftleft.ai/auth/login
Content-Type: application/json
{
"email": "demo@totalshiftleft.ai",
"password": "demo123"
}
응답(Response):
{
"access_token" : "eyJhbGciOi...",
"refresh_token" : "eyJhbGciOi...",
"expires_in" : 900
}
실제로 실행하여 실제 토큰이 돌아오는 것을 보고 싶으신가요?
👉 브라우저에서 실시간으로 실행해 보세요 — 가입이 필요 없습니다.
그 다음, 토큰을 사용하여 보호된 엔드포인트(protected endpoint)를 호출합니다:
GET https://demo.totalshiftleft.ai/api/v1/me
Authorization: Bearer eyJhbGciOi...
Authorization 헤더를 제거하면 401 에러를 받게 됩니다. 이것이 전체 흐름입니다.
서버가 토큰을 검증하는 방법
모든 요청마다 서버는 다음 체크리스트를 실행합니다:
- Authorization: Bearer <token> 헤더를 읽습니다.
- 토큰을 헤더(header), 페이로드(payload), 서명(signature)으로 분리합니다.
- alg가 예상되는 알고리즘과 일치하는지 확인합니다 — none을 명시적으로 거부해야 합니다.
- 비밀키(secret key) 또는 공개키(public key)를 사용하여 header.payload에 대해 서명을 재계산합니다.
- 계산된 서명을 제공된 서명과 비교합니다 — 상수 시간(constant time) 내에 수행해야 합니다.
- 페이로드를 파싱(parse)합니다.
- exp가 미래인지, nbf가 과거인지 확인합니다.
- 선택적으로 iss, aud, jti를 취소 목록(revocation list)과 대조하여 확인합니다.
- 이제서야 클레임(claims)을 신뢰할 수 있는 것으로 취급합니다.
3단계 또는 4단계를 건너뛰는 것이 전형적인 취약점이며, 여러 주요 라이브러리들이 이 상태로 배포된 적이 있습니다.
테스트할 수 있어야 하는 7가지 JWT 취약점
- alg: none. 공격자가 헤더를 {"alg":"none"}으로 변경하고, 서명을 제거한 뒤, 임의의 페이로드를 위조합니다. 이는 라이브러리가 none을 허용할 때만 작동합니다. 이를 명시적으로 거부하십시오.
alg 혼동 (RS256 → HS256). 공격자가 alg를 RS256에서 HS256으로 변경하고, 서버의 공개 키를 마치 HMAC 비밀 키인 것처럼 사용하여 서명합니다. 미숙한 라이브러리들은 이를 공개 키(공개되어 있는 것이 당연하므로)를 대상으로 한 HMAC으로 검증하고 수락해 버립니다. 서버 측에서 항상 알고리즘을 고정(pin)하십시오. 헤더의 alg를 절대 신뢰하지 마십시오.
-
취약한 HMAC 비밀 키. 8자리의 비밀 키는 단 하나의 유효한 토큰만으로도 오프라인에서 무차별 대입 공격(brute-force)이 가능합니다. 256비트 이상의 엔트로피(entropy)를 사용하십시오.
-
긴 만료 시간. exp가 30일 뒤로 설정되어 있다면, 탈취된 토큰은 30일 동안 사용 가능함을 의미합니다. 액세스 토큰(Access token)은 약 15분 정도 유지되어야 하며, 수명이 긴 리프레시 토큰(refresh token)이 나머지를 담당해야 합니다.
-
폐기(revocation) 불가. JWT는 설계상 상태를 유지하지 않는(stateless) 방식이기에 폐기가 어렵습니다. 완화 방법: 짧은 만료 시간 + 리프레시 토큰 사용, jti 블랙리스트, 또는 사용자별 "X 시점 이전에 발급된 토큰은 무효"와 같은 타임스탬프 활용.
-
localStorage에 JWT 저장. XSS 공격에 취약합니다. 브라우저 클라이언트의 경우 httpOnly, Secure, SameSite 쿠키를 사용하십시오.
-
과도한 클록 스큐(clock skew). 5분 정도의 오차 범위는 괜찮습니다. 24시간의 오차 범위는 취약점입니다.
실제로 테스트해야 할 사항
JWT로 보호되는 API에 대한 테스트를 작성하거나 QA를 수행 중이라면, 제가 실제로 사용하는 체크리스트는 다음과 같습니다.
해피 패스 (Happy paths)
- 유효한 자격 증명으로 로그인 → access_token, refresh_token, expires_in과 함께 200 응답.
- 유효한 토큰을 사용하여 보호된 엔드포인트 접근 → 200.
- 토큰에 예상되는 클레임(claims: sub, email, role)이 포함됨.
인증 실패 케이스 (Authentication negatives)
- Authorization 헤더 없음 → 401.
- 잘못된 스킴 (Bearer 대신 Basic 사용) → 401.
- 잘못된 형식의 토큰 (세그먼트 누락) → 401.
- 만료된 토큰 (exp가 과거 시간) → 구분 가능한 코드(예: TOKEN_EXPIRED)와 함께 401.
- 아직 유효하지 않은 토큰 (nbf가 미래 시간) → 401.
- 잘못된 비밀 키로 서명된 토큰 → 401.
- alg: none 토큰 → 401. 이는 보안상 매우 중요한 테스트입니다. 대부분의 JWT 라이브러리가 언젠가는 이 버그를 겪었습니다.
- 변조된 페이로드 (클레임을 수정하고 기존 서명을 유지) → 401.
인가 실패 케이스 (Authorization negatives)
- role: user인 유효한 토큰으로 관리자 엔드포인트 접근 → 403.
- 유효한 토큰이지만 tenant_id가 해당 리소스의 소유자가 아님 → 403 또는 404 (둘 다 방어 가능한 선택이므로 문서화하십시오).
Edge cases (예외 사례)
- 다른 iss(발행자)에 의해 발행된 토큰 → 401.
- aud(대상자)가 이 API와 일치하지 않는 토큰 → 401.
- 거대한 페이로드(10 KB의 커스텀 클레임)를 가진 토큰 → 정상 작동하거나 413 반환.
- 여러 개의 Authorization 헤더 → 동작 방식을 문서화하십시오 (대부분의 스택은 첫 번째 것을 취합니다).
Refresh flow (리프레시 흐름)
- 유효한 리프레시 토큰 → 새로운 access + refresh 쌍 발급; 기존 리프레시 토큰은 이제 무효화됨.
- 오래된 리프레시 토큰 재사용 → 401 (토큰 탈취 감지).
- 사용자가 비밀번호를 변경한 후의 리프레시 토큰 → 401. 비밀번호 변경은 토큰을 무효화해야 합니다.
Pen-tester checklist (모의 해킹 전문가 체크리스트 - 이것을 가져가세요)
- JWT를 복사하고, alg를 none으로 변경하고, 서명을 제거합니다 — 여전히 작동하나요? 만약 그렇다면: 심각한 버그(critical bug)입니다.
- 만약 alg가 RS256이라면, 서버의 공개 키를 사용하여 HS256으로 다시 서명해 보세요 — 작동하나요? 만약 그렇다면: 심각한 버그입니다.
- 페이로드를 디코딩합니다. 그곳에 있어서는 안 될 내용(비밀번호, PII(개인 식별 정보), 아키텍처를 유출하는 내부 ID 등)이 있습니까? 데이터 노출(data exposure)로 보고하십시오.
- 토큰에 jti가 포함되어 있습니까? 포함되어 있지 않다면, 폐기(revocation) 기능이 구현되지 않았을 가능성이 높습니다.
- 시스템 시계를 exp(만료 시간) 이후로 설정합니다 — 토큰이 여전히 작동하나요? 시계 왜곡(Clock-skew) 취약점입니다.
Try it yourself (직접 시도해 보세요)
JWT에 대해 읽는 것도 좋지만, 검증에 실패하는 것을 실시간으로 보는 것이 더 좋습니다. 실행 가능한 로그인, 실제 보호된 엔드포인트, 그리고 샌드박스를 대상으로 실시간으로 진행되는 alg: none 공격을 포함한 전체 레슨은 여기에서 가입 없이 무료로 제공됩니다: 👉 Learn JWT Authentication — runnable lesson
이것은 REST, GraphQL, SOAP, OAuth2, 계약 테스트(contract testing), 그리고 AI 지원 테스트를 다루는 32강 무료 API 테스트 코스의 일부입니다. 모든 레슨에는 라이브 샌드박스를 대상으로 실행 가능한 예제가 포함되어 있습니다: totalshiftleft.ai/learn .
운영 환경에서 여러분이 경험한 가장 좋아하는 JWT footgun(실수 유발 요소)은 무엇인가요? 댓글로 남겨주세요 — 다음 포스트인 리프레시 토큰 회전(refresh-token rotation) 패턴에 관한 글을 위해 수집하고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기