JWT 해부: 헤더, 페이로드, 서명, base64url, 그리고 디코딩이 검증이 아닌 이유

세 개의 점: JSON Web Token의 구조

JSON Web Token은 점으로 구분된 세 개의 base64url 인코딩 값으로 이루어진 문자열입니다: base64url(헤더).base64url(페이로드).base64url(서명). 2015년 5월에 공개된 RFC 7519가 이 형식을 정의합니다. Authorization 헤더, 쿠키, URL 쿼리 파라미터 중 어디에 나타나든 모든 JWT는 이 구조를 따릅니다. 세 부분은 독립적으로 디코딩할 수 있으며, 서명이 암호학적 보증으로 이들을 결합합니다.

이 형식은 간결하고 URL 안전하도록 설계되었습니다. JWT 이전에는 API 인증이 서버 측에 저장된 불투명한 세션 식별자에 의존하는 경우가 많아 매 요청마다 데이터베이스 조회가 필요했습니다. JWT는 클레임을 토큰 자체에 직접 포함시켜 무상태(stateless) 서버가 조회 없이 검증할 수 있게 합니다. 이 편리함에는 책임이 따릅니다: 서버가 즉시 취소할 수 있는 세션 토큰과 달리, 서명된 JWT는 서버가 거부 목록을 관리하지 않는 한 exp 클레임이 만료될 때까지 유효합니다.

base64url 인코딩: base64의 URL 안전 변형

표준 base64는 64개의 문자로 이진 데이터를 인코딩합니다: A–Z, a–z, 0–9, +, /, = 패딩 포함. 이 중 두 문자(+/)는 URL과 HTTP 쿼리 문자열에서 특별한 의미를 가집니다. RFC 4648 섹션 5에 정의된 base64url+-로, /_로 대체하고 = 패딩을 완전히 생략합니다. 그 결과는 추가 인코딩 없이 URL, HTTP 헤더, 쿠키에서 직접 사용할 수 있습니다.

팽창률은 표준 base64와 동일합니다: 입력 3바이트가 4개의 base64url 문자가 됩니다——4/3 배수, 약 33% 증가. 인코딩은 키 없이 역산할 수 있으므로 JWT를 가진 누구나 헤더와 페이로드를 디코딩하여 읽을 수 있습니다——base64url은 인코딩이지 암호화가 아닙니다.

헤더: 알고리즘 선택과 토큰 유형

헤더는 수신자에게 토큰 처리 방법을 알려주는 JSON 객체입니다. 최소 헤더에는 두 개의 필드가 있습니다: alg(서명 알고리즘)와 typ(표준 토큰의 경우 항상 "JWT"). RFC 7518이 알고리즘 식별자를 정의합니다. 가장 일반적인 것들은: HS256——HMAC-SHA256, 서명과 검증에 동일한 비밀을 사용하는 대칭 알고리즘; RS256——SHA-256과 함께하는 RSA-PKCS1v1.5, 비대칭, 개인 키로 서명하고 공개 키로 검증; ES256——P-256과 SHA-256을 사용하는 ECDSA, 역시 비대칭이지만 RSA보다 짧은 서명.

대칭 알고리즘과 비대칭 알고리즘의 선택은 실질적인 아키텍처 결과를 낳습니다. HS256을 사용하면 토큰을 검증해야 하는 모든 서비스가 공유 비밀을 소유해야 합니다. RS256 또는 ES256을 사용하면 발급자는 개인 키를 비공개로 유지하고 공개 키만 게시하여(보통 JWKS 엔드포인트를 통해) 검증 서비스가 서명 자료를 다루지 않아도 됩니다. 멀티 서비스나 타사 통합에는 비대칭 알고리즘이 권장됩니다.

페이로드: 등록된 클레임과 그 의미

페이로드는 클레임을 담은 JSON 객체입니다——엔티티(보통 인증된 사용자)에 관한 주장과 토큰 자체의 메타데이터입니다. RFC 7519 섹션 4.1은 표준화된 의미를 가진 7개의 등록된 클레임 이름을 정의합니다: iss(발급자), sub(주체, 보통 사용자 ID), aud(대상, 의도된 수신자), exp(만료 시간, 이 NumericDate 이후에는 토큰이 수락되어선 안 됨), nbf(유효 시작 시간, 이 NumericDate 이전에는 수락 불가), iat(발급 시각), jti(JWT ID, 재전송 공격 방지에 유용한 고유 식별자).

NumericDate는 1970-01-01T00:00:00Z UTC(유닉스 에포크)로부터의 초 단위 값이며, 밀리초가 아닙니다. 이는 JavaScript의 Date.now()(밀리초 반환)에 익숙한 개발자들이 토큰 검증 코드를 작성할 때 자주 발생하는 버그의 원인입니다. 표준 JWT의 페이로드는 서명되어 있지만 암호화되지 않습니다——토큰을 가로챈 누구나 모든 클레임을 읽을 수 있습니다. 페이로드에 민감한 데이터가 포함된 경우 JWE(RFC 7516)로 암호화하거나 TLS를 통해서만 전송해야 합니다.

서명: 무엇을 보호하는가——그리고 alg:none의 함정

서명은 서명 입력에 대해 계산됩니다: base64url(헤더) + "." + base64url(페이로드). HS256의 경우 서명은 HMAC-SHA256(비밀, 서명_입력)을 base64url 인코딩한 것입니다. 서명 입력에 헤더와 페이로드가 모두 포함되어 있으므로, 어느 한 부분의 변경——단 한 글자라도——이 서명을 무효화합니다. 서명을 올바르게 검증한 서버는 토큰이 발급 이후 변조되지 않았음을 확신할 수 있습니다.

2015년 여러 JWT 라이브러리에서 잘 알려진 취약점 클래스가 발견되었습니다: alg:none 공격. RFC 7519는 기술적으로 "alg":"none"을 서명 없는 비보안 JWT를 나타내는 것으로 허용합니다. 결함: 일부 라이브러리가 (미검증된) 헤더에서 alg 필드를 읽어 검증 경로를 선택했습니다——공격자가 헤더를 "alg":"none"으로 변경하고 서명을 제거하면 라이브러리는 어떤 서명도 확인하지 않고 토큰을 유효하다고 선언했습니다. CVE-2015-9235는 인기 있는 node-jsonwebtoken 라이브러리에서 이 문제를 문서화합니다. 해결책: 검증 측은 예상 알고리즘을 명시적으로 지정하고 헤더에 다른 알고리즘이 명시된 모든 토큰을 거부해야 합니다.

디코딩 vs 검증: 이 차이가 중요한 이유

JWT를 디코딩한다는 것은 세 부분에 base64url 복호화를 적용하여 JSON 헤더, 페이로드, 원시 서명 바이트를 복원하는 것입니다. 키가 필요 없습니다. JWT 문자열을 가진 누구나 디코딩하여 클레임을 읽을 수 있습니다. 이는 의도된 설계입니다——JWT는 불투명한 컨테이너가 아닙니다. 인증 문제를 디버깅하는 개발자, 액세스 로그를 읽는 운영 엔지니어, 시스템을 감사하는 보안 연구원: 모두 보유한 JWT를 디코딩할 수 있습니다. 이것이 JWT 디코더 도구가 유용한 이유입니다.

JWT를 검증한다는 것은 정확한 헤더와 페이로드 바이트에 대해 올바른 키로 서명이 생성되었는지 확인하는 것입니다. 검증 성공은 두 가지를 증명합니다: 토큰이 서명 키 보유자에 의해 생성되었다는 것(진정성)과 서명 이후 헤더와 페이로드 모두 변경되지 않았다는 것(무결성). 검증에는 키가 필요합니다——HMAC 알고리즘에는 공유 비밀, RSA/ECDSA에는 공개 키.

치명적인 실수는 검증 없이 디코딩된 JWT를 신뢰하는 것입니다. localStorage의 JWT에서 클레임을 읽고 서버 측 서명 검증 없이 이를 신뢰하는 클라이언트는 누구나 자유롭게 위조할 수 있는 데이터를 신뢰하는 것입니다. 공격자는 {"sub":"admin","role":"superuser"}를 만들고, 임의의 헤더로 base64url 인코딩하고, 가짜 또는 빈 서명을 붙일 수 있습니다——이것은 완벽하게 디코딩될 것입니다. 권한 결정은 항상 서버 측에서 암호학적 검증 후에 이루어져야 합니다. JWT 디코더 도구는 검사와 디버깅에 적합한 도구이지만, 실제 애플리케이션에서 서버 측 검증을 대체할 수 없습니다.