JWT 디코더
예상 결과: 헤더는 HS256, 페이로드는 sub = demo-user입니다. 서명은 의도적으로 잘못되었으며 로그인 토큰이 아닙니다.
제한 사항: 디코딩만으로 인증되지 않습니다. 서명 검사는 HS256/384/512를 지원합니다. 앱은 발급자, 대상 및 시간 클레임도 검증해야 합니다.
디코딩은 검증이 아닙니다. JWT 페이로드는 누구나 읽을 수 있고, 정상적으로 디코딩되는 위조 토큰도 만들 수 있습니다. 디코딩 결과는 토큰이 무엇을 주장하는지 보여줄 뿐 그 내용이 참인지는 알려주지 않습니다. 모든 처리는 브라우저에서 이루어지며 토큰은 어디로도 전송되지 않습니다.
입력한 내용은 저장, 기록, 공유되지 않습니다.
JWT를 붙여넣으면 헤더와 페이로드, 만료 시각을 확인할 수 있습니다. 디코딩은 브라우저에서만 이루어지므로 토큰이 기기를 벗어나지 않습니다.
사용 방법
- 입력창에 토큰을 붙여넣습니다.
- 헤더, 페이로드, 시간 클레임이 아래에 표시됩니다.
- 필요하면 비밀 키를 입력해 HS256 또는 HS512 서명을 검증합니다.
세 세그먼트 읽는 법
JWT는 base64url로 인코딩된 세 세그먼트를 점으로 이은 것입니다. 여기서 base64url은 패딩 없이 쓰므로, 일반 Base64처럼 세그먼트가 등호로 끝나지 않습니다.
첫 번째 세그먼트는 헤더입니다. alg에 서명 알고리즘을, 보통 typ에 타입을, 때로는 kid에 키 식별자를 담은 작은 JSON 객체이며, kid가 있으면 검증하는 쪽이 여러 키 중 어느 것으로 확인할지 알 수 있습니다. 두 번째는 클레임을 담은 JSON 객체인 페이로드입니다. 세 번째는 서명으로, JSON이 아니라 원시 바이트이므로 여기서는 16진수로 보여 줍니다.
서명은 앞의 두 세그먼트를 보이는 그대로 점으로 이은 것, 즉 그것이 나타내는 JSON이 아니라 인코딩된 텍스트를 대상으로 합니다. 그래서 토큰을 보기 좋게 정리하거나, 다시 직렬화하거나, 클레임 순서를 바꾸면 무효가 됩니다. 이 디코더가 클레임을 보여 주면서도 원본 텍스트를 중요한 것으로 취급하는 이유입니다.
디코딩은 검증이 아니다
JWT에서 가장 흔히 오해하는 부분이므로 에두르지 않고 말하겠습니다. 페이로드는 인코딩되었을 뿐 암호화되지 않았습니다. 토큰을 가진 사람은 누구나 모든 클레임을 읽을 수 있습니다. 그리고 누구든 페이로드에 원하는 내용을 쓴 토큰을 만들 수 있으며, 그 토큰은 여기서든 어디서든 완벽하게 디코딩됩니다.
디코딩한 토큰이 주는 것은 주장의 모음일 뿐, 그 어떤 것에 대한 증거도 아닙니다. 증거는 서명이며, 이를 확인하려면 키가 필요합니다. 미리 정한 키와 미리 정한 알고리즘으로 그 확인을 마치기 전까지, 토큰이 알려 주는 것은 누군가 당신이 믿기를 바라는 내용일 뿐입니다.
실무적으로는 이렇습니다. 디코딩한 페이로드로 권한 결정을 내리지 마세요. 디버거에서 토큰 내용을 보려고 디코딩하는 것은 괜찮습니다. 하지만 게이트웨이를 거쳤으니 이미 확인되었으리라 여기고 애플리케이션 코드에서 검증 없이 sub 클레임을 읽으면, 서비스가 위조된 신원을 믿게 됩니다. 이 페이지에서 선택적인 HS256 검증을 제공하는 것은 바로 그 차이를 눈으로 보여 주기 위해서입니다.
클레임, 그리고 확인할 책임은 누구에게 있는가
RFC 7519는 일곱 개의 클레임을 등록합니다. iss는 발급자, sub는 토큰의 대상인 주체, aud는 토큰을 받을 대상, jti는 토큰 자체의 고유 식별자입니다. 세 시간 클레임인 exp, nbf, iat는 NumericDate 값으로, 밀리초가 아니라 유닉스 에포크 이후의 초 단위입니다. 직접 만든 토큰이 거부되는 가장 흔한 이유가 이것입니다.
이 중 스스로 강제되는 것은 하나도 없습니다. exp가 과거인 토큰이 만료된 것은 검증하는 쪽이 시계와 비교해 거부하기 때문일 뿐입니다. 다른 대상에게 발급된 토큰은 아무도 aud를 확인하지 않을 때만 위험한데, 한 서비스용으로 발급된 토큰이 같은 서명 키를 공유하는 다른 서비스에 재사용되는 경로가 바로 이것입니다. 대부분의 라이브러리는 기본적으로 exp를 확인하지만, aud는 기대값을 알려 줄 때만 확인합니다.
발급하는 머신과 검증하는 머신 사이에 시계가 조금 어긋나는 것은 정상이므로, 검증기는 보통 1~2분 정도의 오차를 허용합니다. 이 페이지는 각 시간 클레임을 읽기 쉬운 날짜로 보여 주고, 토큰이 현재 유효 기간 안에 있는지도 알려 줍니다.
JWT가 맞지 않는 경우
JWT는 자신의 클레임을 스스로 담고 다니는 베어러 토큰입니다. 그래서 상태 없는 서비스 간 호출에는 훌륭하지만 세션 수단으로는 어색합니다.
어색한 이유는 폐기입니다. 검증하는 쪽은 키 말고는 아무것도 필요하지 않으므로 저장소를 조회하는 순간이 없고, 따라서 exp 전에 토큰을 무효화할 방법이 없습니다. 사용자를 로그아웃시키거나, 비밀번호를 바꾸거나, 계정을 비활성화해도 이미 발급된 토큰은 만료될 때까지 계속 동작합니다. 흔한 해법인 아주 짧은 유효 기간과 리프레시 토큰의 조합, 또는 요청마다 jti 거부 목록을 확인하는 방식은 모두 무상태로 없애려던 상태를 다시 들여옵니다.
저장 위치도 중요합니다. localStorage에 있는 토큰은 페이지에서 실행되는 모든 스크립트가 읽을 수 있어 크로스 사이트 스크립팅 결함 하나로 유출됩니다. httpOnly 쿠키는 읽을 수 없는 대신 CSRF 방어가 필요합니다. 또한 토큰은 모든 요청의 헤더에 실려 가므로 페이로드가 크면 실제 비용이 생깁니다. 클레임을 잔뜩 넣다 보면 프록시의 헤더 크기 제한에 걸립니다.
평범한 웹 세션이라면 서버 측 저장소와 연결된 무작위 불투명 식별자가 더 단순하고, 즉시 폐기할 수 있으며, 크기도 작습니다.
검증이 순서대로 하는 일
올바른 검증은 정해진 순서가 있는 절차이고, 어느 한 단계를 건너뛴 자리에 알려진 공격들이 자리를 잡습니다. 첫째, 알고리즘을 미리 정해 두고 헤더가 그것과 다른 토큰은 거부합니다. 토큰에서 alg를 읽어 그대로 믿어서는 절대 안 됩니다. 둘째, 키를 고릅니다. 공유 비밀키라면 상수이고, 비대칭 토큰이라면 헤더의 kid에 해당하는 공개키로, 발급자의 JWKS 엔드포인트에서 가져와 캐시해 둔 것입니다.
셋째, 앞의 두 세그먼트를 있는 그대로 사용해 서명을 다시 계산하고 상수 시간으로 비교합니다. 넷째, 서명이 확인된 뒤에야 클레임을 읽습니다. exp와 nbf를 약간의 허용 오차와 함께 시계와 대조하고, iss가 기대하는 발급자인지 확인하고, aud에 자신이 들어 있는지 확인합니다.
설정만 해 두면 대부분의 라이브러리가 이 모든 일을 해 주며, 취약점의 대부분은 애플리케이션이 그중 일부를 직접 처리하면서 생깁니다. 검증 전에 디코딩해 클레임을 읽는 것은 디버거에서는 괜찮지만 요청 처리 과정에서는 위험합니다. 그 시점의 토큰은 여전히 누군가가 그렇다고 주장한 내용일 뿐이기 때문입니다.
OAuth와 OpenID Connect에서 JWT의 자리
OpenID Connect에서 id_token은 JWT이며 클라이언트를 위한 것입니다. 누가, 언제, 어떤 방식으로 로그인했는지를 알려 줍니다. 액세스 토큰은 API를 위한 별개의 물건이고, 많은 공급자가 이를 JWT로 발급하지만 명세가 그것을 요구하지는 않습니다. API가 introspection으로 확인하는 불투명한 문자열이어도 됩니다.
이 구분이 중요한 이유는 API가 무엇을 해야 하는지가 여기서 갈리기 때문입니다. 액세스 토큰이 불투명하면 API는 공급자의 introspection 엔드포인트를 호출하고, JWT라면 공개된 키로 로컬에서 검증합니다. id_token을 액세스 토큰처럼 다루는 것은 흔한 연동 버그입니다. 그것은 다른 대상을 위해 발급된 것이며, 그것을 받아 주는 API는 같은 공급자가 서비스하는 어떤 애플리케이션용으로 발급된 토큰이든 받아들이게 됩니다.
토큰 안의 스코프와 역할은 발급자의 주장이지 권한 자체가 아닙니다. 호출자가 무엇을 할 수 있는지는 여전히 API가 결정합니다. 토큰은 그가 누구인지, 그리고 발급자가 어디까지 보증할 의사가 있었는지만 말해 줍니다.
계속해서 되풀이되는 취약점
alg: none 수법은 토큰이 서명되지 않았다고 선언하는 것으로, 10년쯤 전에는 여러 라이브러리가 이를 받아들였습니다. 알고리즘 혼동은 좀 더 교묘합니다. RS256 토큰을 가져와 헤더를 HS256으로 바꾸고, 발급자의 공개키를 HMAC 비밀키로 삼아 서명하는 것이죠. 토큰에서 알고리즘을 읽는 검증자는 이를 받아들입니다. 공개키는 비밀이 아니기 때문입니다.
현장에서 가장 자주 발견되는 문제는 약한 공유 비밀키입니다. JWT에는 후보 비밀키를 오프라인에서 시험하는 데 필요한 모든 것이 들어 있고, 공격자의 하드웨어가 허용하는 속도만큼 시도할 수 있습니다. 사전에 있는 단어라면 몇 초 만에 뚫리고, 그때부터는 누구나 원하는 클레임을 담은 토큰을 찍어낼 수 있습니다.
그다음은 논리 오류들입니다. aud 검사를 빼먹어 같은 키를 공유하는 다른 서비스의 토큰으로 인증이 통과되는 경우, 입력을 검사하지 않은 채 kid 값으로 경로에서 키를 읽어 오는 경우, 그리고 쿼리 문자열로 전달된 토큰을 받아들여 서버 로그와 브라우저 기록, referrer 헤더에 그대로 남는 경우입니다.
웹 애플리케이션에 세션이냐 토큰이냐
자체 백엔드를 가진 단일 웹 애플리케이션이라면, 불투명한 쿠키 식별자를 쓰는 서버 측 세션이 거의 모든 면에서 더 단순하고 더 낫습니다. 취소가 즉시 적용되고, 쿠키가 작고, 내용이 서버 밖으로 나가지 않으며, 잘못될 서명 자체가 없습니다. 대가는 세션 저장소인데, 대부분의 애플리케이션에서는 테이블 하나나 Redis 키 하나면 됩니다.
JWT가 제 몫을 하는 곳은 검증하는 쪽이 발급자에게 물어볼 수 없는 상황입니다. 신뢰 경계를 넘는 서비스 간 호출, 제3자가 사용하는 API, 왕복 없이 요청을 검증해야 하는 무상태 엣지 같은 경우죠. 이런 것들은 실제 제약이며, 해당된다면 복잡함을 감수할 가치가 있습니다.
대부분의 팀이 도달하는 절충안은 수명이 짧은 액세스 토큰(시간 단위가 아니라 분 단위)과, httpOnly 쿠키에 담고 서버에도 저장해 언제든 취소할 수 있게 한 수명이 긴 리프레시 토큰의 조합입니다. 이렇게 하면 일반적인 경로에서는 무상태 검증을 누리면서도 거절할 수 있는 지점을 남겨 둘 수 있습니다.
브라우저에서 토큰을 어디에 둘 것인가
localStorage는 그 페이지에서 실행되는 어떤 스크립트든 읽을 수 있어서, 같은 오리진 어딘가에 크로스 사이트 스크립팅 결함이 하나만 있어도, 그것이 제3자 분석이나 광고 스크립트 안에 있더라도 토큰이 빠져나갑니다. 많은 튜토리얼이 이것을 기본값으로 삼지만, 선택지 가운데 가장 약한 방법입니다.
httpOnly, Secure, SameSite를 붙인 쿠키는 스크립트가 아예 읽을 수 없어서 그 부류의 공격이 사라지지만, 대신 CSRF 방어가 필요해집니다. 브라우저가 쿠키를 자동으로 붙여 보내기 때문입니다. SameSite=Lax가 대부분을 막아 주고, 나머지는 위조 방지 토큰이 채워 줍니다.
토큰을 메모리에만 두는 방법, 즉 애플리케이션 안의 변수로 갖고 있다가 페이지가 로드될 때 쿠키로 인증하는 엔드포인트를 통해 다시 받아 오는 방식은 흔한 패턴 중 가장 강력하면서 가장 불편합니다. 새로고침할 때마다 갱신을 위한 왕복이 필요하기 때문입니다. 무엇을 고르든 토큰은 여전히 요청마다 헤더에 실려 오가므로, 페이로드를 작게 유지하는 것은 프라이버시뿐 아니라 성능의 문제이기도 합니다.
자주 묻는 질문
- 디코딩되면 그 JWT가 유효한 건가요?
- 아닙니다. JWT에서 가장 중요한 점입니다. 페이로드는 base64url로 인코딩되어 있을 뿐 암호화되거나 보호되지 않습니다. 누구나 읽을 수 있고, 깔끔하게 디코딩되는 위조 토큰도 만들 수 있습니다. 올바른 키로 서명을 검증해야만 진짜이고 변조되지 않았음을 알 수 있습니다.
- 실제 토큰을 여기에 붙여넣어도 안전한가요?
- 디코딩과 검증 모두 이 페이지의 JavaScript에서 실행되며 업로드, 기록, 전송이 없습니다. 다만 JWT는 살아 있는 자격 증명입니다. 다른 곳에서 유출되었을 가능성이 있다면 어디에 붙여넣었는지와 무관하게 폐기하세요.
- 왜 HS256과 HS512만 검증할 수 있나요?
- 대칭 방식이라 같은 비밀 키로 서명과 검증을 모두 하므로 입력한 키로 로컬에서 확인할 수 있습니다. RS, ES, PS는 비대칭 방식이라 보통 JWKS 엔드포인트에서 가져오는 발급자의 공개 키가 필요하며 별개의 작업입니다.
- exp, iat, nbf는 무슨 뜻인가요?
- 등록된 클레임이며 값은 NumericDate로, 밀리초가 아니라 유닉스 에포크 기준 초 단위입니다. exp는 만료 시각, iat는 발급 시각, nbf는 사용 가능한 가장 이른 시각입니다. 이 도구는 각각을 읽기 쉬운 날짜로 보여줍니다.
- alg: none은 무엇을 의미하나요?
- 서명되지 않은 토큰이라는 뜻입니다. 이를 허용하는 라이브러리는 위조 토큰을 통과시킬 수 있어 잘 알려진 공격 패턴이며, 검증하는 쪽에서 거부해야 합니다. 이 도구도 그런 토큰을 검증됨으로 표시하지 않습니다.
마지막 업데이트:
참고 자료
RFC 7519 — JSON Web Token최근 변경
- — 실행 가능한 예제, 예상 결과, 제한 사항 및 참고 링크를 추가했습니다.
진행 단계, 예상 결과, 실제 결과를 적어 주세요. 비밀 정보와 개인 정보는 삭제해 주세요.
