fixmint
KO

JWT 안에는 무엇이 들었을까? 디코딩과 검증이 다른 이유

지은이: · · 4분 읽기

JSON 웹 토큰(JWT)은 점(.)으로 이어 붙인 세 덩어리의 텍스트입니다. 헤더, 페이로드, 서명입니다. 앞의 두 부분은 인코딩되어 있을 뿐 암호화된 것이 아니므로, 토큰을 가진 사람이라면 누구나 내용을 읽을 수 있습니다. 서명은 서버가 토큰을 신뢰할 수 있는 쪽에서 발급했는지, 그리고 누군가 내용을 바꾸지 않았는지 확인하게 해 줍니다. 토큰을 디코딩하면 그 안에 무엇이 적혀 있는지 알 수 있습니다. 그 내용을 믿어도 되는지는 검증해야 알 수 있습니다.

JWT 디코더토큰이 만료됐을까, 아직 유효할까?무료로 사용하기

세 부분

토큰은 header.payload.signature 같은 모양입니다. jwt.io에서 쓰는 유명한 예제 토큰의 첫 부분은 다음과 같습니다:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

앞의 두 부분은 각각 JSON을 base64url로 변환한 것입니다. base64url은 URL에 그대로 넣어도 안전한 텍스트 형식입니다. 첫 부분을 디코딩하면 다음과 같습니다:

{"alg":"HS256","typ":"JWT"}

같은 예제의 페이로드를 디코딩하면 다음과 같습니다:

{"sub":"1234567890","name":"John Doe","iat":1516239022}

페이로드에는 클레임이 담깁니다. 클레임은 사용자나 세션에 관한 진술입니다. 서명은 앞의 두 부분과 키로 계산되므로, 한 글자만 바뀌어도 서명이 맞지 않게 됩니다. base64url은 키 없이도 되돌릴 수 있으니, 비밀번호나 개인키, 개인정보는 페이로드에 절대 넣지 마세요.

등록된 클레임

JWT의 표준인 RFC 7519는 몇 가지 짧은 클레임 이름을 예약해 두었습니다:

  • iss (발급자): 토큰을 만든 주체입니다.
  • sub (주체): 토큰이 누구에 관한 것인지 나타내며, 보통 사용자 ID입니다.
  • aud (대상): 토큰을 받을 서비스입니다.
  • exp (만료 시각): 이 시각 이후 토큰은 유효하지 않습니다.
  • nbf (이전 불가 시각): 토큰이 처음으로 유효해지는 시각입니다.
  • iat (발급 시각): 토큰이 만들어진 시각입니다.

시각은 1970년 1월 1일 UTC부터 경과한 초 단위이며, 이를 유닉스 시간이라고 합니다. 예제에는 exp가 없으므로 표준대로라면 만료되지 않습니다. 실제 토큰에는 만료 시각이 있어야 합니다. role이나 email 같은 다른 이름은 애플리케이션이 정합니다.

디코딩은 검증이 아닙니다

디코딩은 점을 기준으로 토큰을 나누고, base64url을 되돌린 뒤 JSON을 읽는 과정입니다. 토큰이 주장하는 내용을 보여 줄 뿐이며, 위조된 토큰에도 똑같이 적용됩니다. 누구든지 어떤 서명을 붙이든 원하는 클레임을 쓸 수 있기 때문입니다.

검증은 올바른 키로 서명을 다시 계산하여 일치하는지 확인하는 것입니다. 또한 현재 시각이 exp와 nbf 사이에 있는지, iss와 aud가 서비스가 기대하는 값인지도 확인합니다. 검증 없이 role 클레임을 읽어 쓰는 앱은 토큰을 쓴 사람이 누구든 그대로 믿는 셈입니다. 흔히 저지르는 실수입니다.

HS256, RS256, ES256

alg 값은 서명 방식을 가리킵니다. 자주 쓰이는 것은 세 가지입니다:

  • HS256은 SHA-256을 쓰는 HMAC입니다. 하나의 공유 비밀키로 서명하고 검증하므로, 그 비밀키는 토큰을 발급하는 서버 안에만 있어야 합니다.
  • RS256은 SHA-256을 쓰는 RSA 서명입니다. 발급자는 개인키로 서명하고, 공개키를 가진 누구나 검증할 수 있습니다. 다른 서비스는 토큰을 만들 능력 없이 검사만 할 수 있습니다.
  • ES256은 P-256 곡선과 SHA-256을 쓰는 ECDSA 서명입니다. 원리는 RS256과 같지만 키와 서명이 더 작습니다.

한 서비스가 토큰을 발급하고 검사할 때는 HS256을 쓰세요. 다른 서비스가 자신이 발급하지 않은 토큰을 검사해야 한다면 RS256이나 ES256을 쓰세요.

헤더를 믿을 때 생기는 문제

헤더는 토큰을 보낸 쪽이 작성하므로, 검증하는 쪽은 헤더가 규칙을 정하도록 내버려 두면 안 됩니다.

  1. alg: none. 서명이 없는 토큰도 "alg":"none"이라고 선언할 수 있습니다. 일부 라이브러리는 한때 이런 토큰을 받아들이고 검사를 건너뛰었습니다. 허용할 알고리즘은 토큰에서 가져올 것이 아니라 직접 코드에 정해 두세요.
  2. 알고리즘 혼동. 서버는 RS256을 기대하며 공개키를 가지고 있습니다. 공격자는 그 공개키를 HMAC 비밀키로 삼아 서명한 HS256 토큰을 보냅니다. 헤더가 지정한 알고리즘을 그대로 받아들이는 검증기는 이 토큰을 통과시킵니다. 각 키는 정확히 한 가지 알고리즘에만 묶어 두세요.
  3. 약한 비밀키. 공격자는 헤더, 페이로드, 서명을 모두 가지고 있으므로 후보 비밀키를 오프라인에서 시험해 볼 수 있습니다. 짧은 비밀키나 흔한 단어는 그런 목록에 금방 뚫립니다. RFC 7518은 HS256 키가 최소 256비트이며 무작위여야 한다고 말합니다.

운영 중인 토큰을 웹사이트에 붙여 넣기

유효하고 만료되지 않은 토큰은, 만료되기 전까지 그것을 가진 사람이라면 누구든 사용할 수 있습니다. 비밀번호처럼 다루세요. 토큰을 어떤 사이트에 붙여 넣기 전에, 그 사이트가 토큰을 서버로 보내는지 확인하세요. 브라우저 탭 안에서만 실행되는 도구라면 보낼 이유가 없습니다. 브라우저의 개발자 도구에서 네트워크 요청을 살펴보면 어떤 페이지든 확인할 수 있습니다.

서버가 목록을 따로 보관하지 않는 한, 토큰은 보통 만료되기 전에 취소할 수 없습니다. 활성 토큰이 유출되었다면 서비스에 세션을 취소하거나 키를 교체해 달라고 요청하세요. 가능하면 디버깅에는 테스트 환경의 토큰을 쓰세요. JWT 디코더는 브라우저 탭에서 세 부분을 읽으며, 붙여 넣은 내용은 어디로도 전송되지 않습니다.

웹훅: 공유 비밀키를 쓰는 같은 원리

대부분의 웹훅 제공자는 요청 본문의 HMAC을 헤더에 실어 보냅니다. 이 HMAC은 제공자와 공유한 비밀키로 계산됩니다. 수신 측은 같은 HMAC을 계산해 두 값을 비교합니다. 흔한 실패 원인은 대개 사소한 데 있습니다. 해싱하기 전에 JSON을 다시 포맷해서 바이트가 바뀌거나, 테스트용 비밀키를 실제 트래픽에 쓰거나, 서명을 일반 문자열 비교로 확인하는 경우입니다. 일반 문자열 비교는 타이밍 정보가 새어 나갈 수 있습니다. 상수 시간 비교를 사용하세요.

웹훅 서명 검사기는 붙여 넣은 비밀키와 본문으로 서명을 계산합니다. GitHub, Stripe, Slack, Shopify 또는 그 밖의 HMAC 방식을 지원하므로, 두 값이 일치하는지 바로 확인할 수 있습니다.

빠른 답변

JWT는 암호화되어 있나요?

아니요. 표준 JWT는 서명된 것이며, 누구나 헤더와 페이로드를 읽을 수 있습니다. 변조를 막아 주는 것은 서명뿐입니다.

페이로드를 고치면 어떻게 되나요?

서명이 맞지 않게 되므로, 검증하는 서버는 그 토큰을 거부합니다. 클레임을 읽는 것은 서명을 검사하지 않으므로, 디코더는 여전히 고친 내용을 그대로 보여 줍니다.

AI 도움을 받아 영어 원문에서 번역했어요.

댓글

    아직 댓글이 없어요. 첫 댓글을 남겨 보세요.

    별점 (선택)

    이메일은 공개되거나 보관되지 않아요.

    Agentic AI in the Browser 표지Agentic AI in the Browser

    브라우저에서 바로 돌아가고 개인정보를 지키는 AI 워크플로의 활용 사례를 담았어요. fixmint가 바탕으로 삼는 '내 기기에서 처리한다'는 같은 원리예요.

    $4.99Gumroad에서 받기영어로 보기