4P by GN⁺ | ★ favorite | 댓글 2개
  • JWT는 사용자를 로그인 상태로 유지하는 용도에 맞지 않으며, 이 목적에는 일반 쿠키 세션이 더 적합함
  • JWT 사양은 약 5분 이하의 짧은 수명 토큰을 전제로 하며, 세션은 그보다 긴 수명이 필요함
  • 안전한 무상태 인증은 실현하기 어렵고, 토큰을 안전하게 다루려면 결국 일부 상태 저장소가 필요함
  • 단순 세션 토큰만 담는 JWT는 일반 세션 쿠키보다 비효율적이고 유연성이 낮으며, 인증 정보는 localStorage나 sessionStorage에 저장하지 않아야 함
  • 짧은 수명의 서명 토큰이 필요할 때는 보안을 위해 설계된 PASETO가 더 나은 선택이지만, 세션 용도로는 쓰지 않아야 함

핵심 요약

  • JWT는 사용자를 로그인 상태로 유지하는 데 쓰지 않아야 하며, 그 목적에는 일반 쿠키 세션이 더 나은 도구임
  • JWT는 이 목적을 위해 설계되지 않았고 안전하지 않으며, 로그인 세션 유지에는 정규 쿠키 세션이 더 적합함
  • 관련 주제로, JWT 토큰을 포함한 인증 자격 증명은 localStorage나 sessionStorage에 저장하지 않아야 함
  • JWT 관련 발표를 볼 수 있지만, CSRF 보호 같은 다른 주제는 대체로 간략히 다뤄지므로 다른 출처에서 별도로 학습해야 함
  • 영상 끝부분의 “유효한” JWT 사용 사례도 더 낫고 안전한 도구로 쉽게 처리할 수 있으며, 구체적으로 PASETO가 해당함

JWT를 피해야 하는 이유

  • JWT 사양은 약 5분 이하의 매우 짧은 수명 토큰만을 위해 설계됐고, 세션은 그보다 긴 수명이 필요함
  • 안전한 방식의 무상태 인증은 가능하지 않으며, 토큰을 안전하게 처리하려면 일부 상태가 반드시 필요함
    • 데이터 저장소가 필요하다면 일부 토큰 상태만 다루기보다 모든 데이터를 저장하는 편이 더 나음
    • 관련 문제는 http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/에서 더 자세히 다뤄짐
    • 실제로 JWT를 이런 방식으로 쓰는 애플리케이션이 있지만, 그런 애플리케이션은 결함이 있으므로 같은 실수를 반복하지 않아야 함
  • 단순한 세션 토큰만 저장하는 JWT는 일반 세션 쿠키보다 비효율적이고 덜 유연하며, 추가 이점을 얻지 못함
  • JWT 사양 자체는 보안 전문가들에게 신뢰받지 못하므로, 보안과 인증 관련 용도 전체에서 배제해야 함

반론

  • “Google도 JWT를 쓴다”는 반론은 브라우저의 사용자 세션에 해당하지 않음
    • Google은 브라우저 사용자 세션에 JWT를 쓰지 않고 일반 쿠키 세션을 사용함
    • JWT는 한 서버나 호스트의 로그인 세션을 다른 서버나 호스트의 세션으로 전달하는 Single Sign On 전송 수단으로만 쓰임
    • 이 사용 방식은 JWT의 합리적인 사용 사례 범위에 들어감
    • Google은 더 안전한 JWT 구현을 만들고 유지할 보안 전문가 자원을 갖고 있음
    • Google의 JWT는 사실상 다른 곳의 JWT와 같지 않음
  • “무상태가 더 낫다”는 반론은 안전한 인증 요구와 맞지 않음
    • 막대한 자원 없이는 진정한 무상태 인증을 안전하게 운영할 수 없음
    • 관련 논의로 Stateless is a lie를 참고할 수 있음
  • “세션 설정 방법을 모른다”는 문제는 대부분의 웹 서버 프레임워크 문서와 구현으로 해결할 수 있음
    • 세션 기술은 특별히 새롭지 않기 때문에 세션을 설명하는 글을 자주 보지 못함
    • 세션 구현체의 문서만으로도 설정 과정을 따라갈 수 있어야 함
    • 거의 모든 웹 서버 프레임워크에는 세션 구현이 들어 있으며, 기본 활성화가 아니더라도 보통 쉽게 활성화할 수 있음
    • Express와 다른 Node.js 프레임워크는 높은 모듈성과 단일 목적 성격 때문에 어느 정도 예외에 가까움
    • Express에서는 express-session 미들웨어와 저장소에 맞는 store connector를 사용하면 됨
    • Postgres, MySQL 또는 가능하면 SQLite와 함께 connect-session-knex 사용을 권장함

단기 토큰

  • 어떤 용도에 짧은 수명의 서명 토큰이 필요하다면, 보안을 위해 설계된 PASETO라는 더 나은 사양이 있음
  • PASETO를 쓰더라도 세션 용도로는 사용하지 않아야 함

세션 작동 방식

  • 세션 작동 방식을 더 배우려면 joepie91의 gist를 확인하는 편이 좋음

댓글과 토론

  1. JWT는 토큰 암호화 및 DB 질의를 줄이는 방식이지, 쿠키 인증과 대척점에 있는 개념이 아닙니다. JWT를 secure 쿠키에 저장하면 탈취 위험이 레거시 쿠키 인증 방식과 같습니다

  2. JWT expired 를 시키려고 만료 목록을 관리하는게 성능 측면에서 이점이 있습니다. 만료 정보만 redis에서 질의하는 것과 전 회원을 DB 질의 하는것의 비용차이가 존재합니다.

수만번의 횟수로 10만 회원 row의 index 기반 질의 (레거시 쿠키 방식)
vs
수만번의 횟수로 redis에서 만료 목록 50개 질의 (JWT 즉시 expire 방식)

JWT의 이점이 있긴 합니다. 소규모 환경에서 차이가 덜 날 뿐입니다

Hacker News 의견들
  • 필요한 단서가 빠졌음: 브라우저 기반 사용자 세션에 대한 얘기임
    서비스 간 통신에는 JWT를 잘 쓸 수 있는 경우가 많음
    덧붙이면, 링크된 글 일부를 읽어봤는데 예를 들어 https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba... 같은 글이 있음. JWT가 그렇게 끔찍하게 안전하지 않은 표준이라면 AWS STS의 AssumeRoleWithWebIdentity를 해킹하는 방법을 공개하거나, 공개하지 말고 Fortune 500 기업들의 프로덕션 AWS 계정마다 암호화폐 채굴기를 띄우면 됨. JWT가 그렇게 불안전하다니, 결국 성공하면 알려주길 바람 /비꼼

    • 이게 딱 합리적인 결론임. JWT는 브라우저의 사용자 세션 용도로는 잘못된 도구라는 데 동의함
      JWT의 서명·암호화 부분은 복잡하고, 흔한 JWT 라이브러리들도 이제야 대체로 정신을 차렸지만 예전에는 그렇지 않았음. "none" 알고리즘을 받아들이는 라이브러리도 많았고 [1], 공개 키를 공유 비밀처럼 써서 공격자가 토큰을 위조할 수 있게 한 경우도 있었음 [2]. 링크된 글이 비판하는 복잡성이 그대로 낳은 결과임
      JWT는 사용자 세션에서 원하는 기능도 못 하는 경우가 있음. 어딘가에 폐기 목록을 두지 않으면 무효화할 수 없음. 그런데 요청마다 식별자를 폐기 목록과 대조해야 한다면, 차라리 불투명한 세션 ID를 쓰고 요청마다 조회하면 됨. 물론 짧은 수명 토큰을 쓰고 계속 갱신할 수는 있지만, 어차피 상태를 유지해야 하는 일반 애플리케이션에서 굳이 그럴 이유가 약함
      다만 분산 시스템이나 기계 간 통신에서는 서명된 토큰이 유용한 경우가 있다는 데 전적으로 동의함. 두 경우를 혼동하지 말아야 함
      [1] https://nvd.nist.gov/vuln/detail/cve-2022-23540
      [2] https://nvd.nist.gov/vuln/detail/CVE-2024-54150
    • 예전의 JWT는 나쁜 기본값의 라이브러리 때문에 문제가 많았음. 몇 년 전에는 다운그레이드 공격도 꽤 흔했음
      지금은 여러 언어의 주요 라이브러리들이 더 정상적인 기본값을 갖추면서, 요즘은 실제로 꽤 안전해졌다고 봄
    • JOSE는 제대로 구현하면 안전하더라도 여전히 문제가 생길 수 있음. 관련 API 표면이 별로인 경우가 많음
      “올바르게 잡고 쓰면 안전하다”가 곧 좋은 설계라는 뜻이라면 X.509 같은 것에도 똑같이 적용될 것임
      많은 경우에는 더 나은 대안이 있음. 표준 세션 토큰이나 API 키는 대부분의 대형 웹사이트에서 널리 쓰이고, 대부분의 용도에 거의 완벽하게 맞음
      이런 표준에 가치가 전혀 없다고 하려는 건 아님. 가장 좋은 점은 ASN.1 인코딩 같은 것 없이 뭔가를 주고받는 기본 표준이라는 점이고, ASN.1 쪽 도구들은 매우 취약하고 버그가 많아 보임
    • 앞부분에는 동의하지만, 덧붙인 내용은 논리적 오류임. 뭔가를 해킹할 수 있어야만 그것이 안전하지 않다고 말할 수 있는 건 아님
      예를 들어 SAML을 어떻게 악용하는지는 모르지만, XML 파서 전체를 공격 표면으로 만들기 때문에 끔찍한 표준이라는 건 알고 있음. 보안 연구자가 아니라 XML 파서에서 취약점을 찾는 법은 모르지만, 공격 표면이 큰 건 나쁘다는 건 알 수 있음
    • 실제 애플리케이션에서 JWT 때문에 생긴 취약점 계보는 길게 이어져 있음
  • JWT가 안전하지 않다니, 신뢰된 RSA/공개키 기반 서명 방식을 써도 그렇다는 건가? 공유 비밀이 아니라도?
    JWT가 너무 오래 산다는 주장도 이상함. JWT 수명을 제한하고 인증 기관에 대해 갱신 모델을 두면 됨. 쿠키 기반 세션을 쓰더라도 결국 어딘가에 저장은 하고 있음. JWT를 5~15분만 유효하게 만들 수 있고, 15분은 Entra를 포함한 여러 권한 부여 시스템의 캐시 시간과 비슷함. 5분짜리 토큰도 갱신 시스템이 있으면 브라우저에서 충분히 쓸 수 있음
    마지막으로, 정체성/인증을 애플리케이션·API 서비스와 분리하는 편을 선호함. 컨텍스트를 외부화할 수 있고, 요청마다 JWT를 처리하는 방식이 간헐적으로 실패할 수 있는 공유 캐시/상태 시스템보다 다루기 쉬움. 서명된 토큰은 알려진 기관에 대해 서명을 검증할 수 있음

    • JWT는 30초 뒤, 심지어 1초 뒤에도 무효가 되게 만들 수 있음. JWT를 만들 때는 audience를 설정해야 함
      그 외에는 서명이 암호학적으로 타당함. 짧은 수명으로 모든 JWT를 매번 검증하면 됨
      참고로 OIDC 토큰은 전부 JWT임
  • 세션과 JWT 폐기 목록을 비교하면, JWT 폐기 목록에 유리한 논리도 있음. JWT에는 제한된 만료 시각이 있으므로, 아직 만료되지 않은 토큰에 대해서만 폐기 목록을 유지하면 됨
    유통 중인 유효 JWT에 비해 폐기된 JWT는 일부일 가능성이 높으니, 요청마다 아주 작은 데이터셋만 조회하면 됨
    세션을 쓰면 유효 세션 목록은 폐기 목록보다 몇 자릿수 더 클 가능성이 높고, 따라서 상태 저장에 따른 조회 비용과 저장 비용이 더 큼
    게다가 글에서는 JWT가 무상태라고 하지만, 보통은 그렇지 않음. 대개 JWT만 검증하는 게 아니라 요청마다 일치하는 신원 객체, 즉 사용자 세부 정보를 가져와 사용자가 여전히 활성 상태인지, 해당 작업을 할 권한이 있는지 확인함. 사용자별 폐기 목록이나 minimum_issued_at 같은 값을 활용해 JWT의 iat 필드를 검증할 수 있음. 이렇게 하면 “모든 기기에서 로그아웃” 패턴도 가능하고, 해당 동작이 사용자의 minimum_issued_at$NOW로 설정하기만 하면 이전 토큰이 전부 폐기됨. 개별 폐기 목록 조회가 필요 없음

    • 사용자 객체를 조회해야 하는 순간, JWT의 핵심 장점은 사라진 것이고 그냥 버려도 됨
    • 세션 데이터 조회는 데이터베이스에서 인덱스를 쓰는 select 한 번이고, 0~1행을 반환함. 대부분의 경우 걱정할 일이 아님
    • 세션에도 만료 시각이 있고, 원하는 대로 설정할 수 있음
  • 이 글은 “왜”에 해당하는 대부분을 다른 블로그 글에 링크하고 있는데, 그 블로그 글은 대체로 “개별 JWT 토큰을 무효화할 수 없다”는 데 불만을 가진 것처럼 보임
    내가 구현할 때마다 일반적인 지침은 어딘가에서 무효화된 nonce를 확인하는 것이었고, 그러면 그 글의 두 번째 주장도 해결됨
    “JWT 명세 자체를 보안 전문가들이 신뢰하지 않는다”는 말은 블로그 글 하나보다 더 많은 근거가 필요해 보임. 그리고 그 글은 대체로 나쁜 구현을 탓하는 것 같은데, 어떤 표준이든 나쁜 구현 문제는 따라다님
    전반적으로, 임의의 gist 링크를 클릭하면서 뭘 기대했는지 모르겠음

    • 초기 구현 중 일부는 헤더에 아무 기관이나 설정할 수 있게 하고 그대로 신뢰했는데, 그건 당연히 처음부터 잘못된 방식임. 신뢰된 또는 “알려진” 기관만 허용하면 많은 맥락상의 우려가 사라짐
      그 밖에도 브라우저에서 더 짧은 수명의 JWT를 충분히 쓸 수 있고, 에이전트가 스스로 갱신하게 만들 수 있음. Azure Entra나 여러 다른 제공자를 쓰면 실제로 그렇게 동작함. JWT를 비교적 짧게, 5~15분 정도로 유지하고 jti 폐기 여부까지 확인할 수 있음
      JWT는 접근 권한 기관을 애플리케이션/API 시스템에서 분리하고 재사용하는 데 매우 유용함. 공격 표면을 옮기되 신뢰할 수 있는 방식으로 옮기는 것임. 전 세계에서 SSH를 포함해 여러 곳에 공개키 방식을 쓰고 있음. 공유 비밀이나 장수명 토큰은 쓰지 않겠지만, 검증되고 알려진 출처에서 온 짧은 수명의 공개키 서명 토큰은 대체로 괜찮음
      오히려 실제로 문제가 되는 건 API 키인 경우가 많음. 방금 구현해야 했는데, 내 경우 API 키도 Bearer 토큰처럼 보이게 했고, 짧은 sak. 접두사 다음에 신원 부분(base64url UUID 바이트), 이어서 비밀값(base64url 바이트)을 둠. 데이터베이스에는 UUID와 비밀값에서 만든 패스프레이즈 수준의 salt+hash를 저장함. 따라서 생성된 API 키는 비밀로 취급해야 하고, 데이터베이스에는 단방향으로만 저장되어 DB 침해가 곧 인증 침해로 이어지지 않음
      그래도 잘 구현된 JWT 솔루션에 문제가 생기는 것보다 API 키 유출이 훨씬 더 가능성이 높음
    • “개별 JWT 토큰을 무효화할 수 없다”는 주장에 100% 동의하지 않음. 구현할 때 무효화된 nonce를 어딘가에서 확인하는 건 내게는 상식이고, 사람들이 그렇게 안 한다는 걸 다시 알 때마다 놀람
  • 이 글을 우연히 보고, 예전에 이 주제로 많은 작업을 했기 때문에 지금 다시 화제가 되는 게 흥미롭다고 생각했음. 그런데 클릭해 보니 작성자가 내 자료 일부에 링크하고 있었음. 정말 오래전 기억이 떠오름
    어쨌든 나보다 훨씬 똑똑한 사람들이 이 주제를 수년간 폭넓게 다뤄왔지만, 2026년에도 JWT는 웹 인증에는 잘못된 도구라고 생각함. 서비스 간 용도로는 괜찮지만, 선택권이 있다면 그냥 PASETO를 쓰는 게 좋음. 많은 문제를 해결해 줌

  • 지금 웹사이트에 알림 푸시용으로 RabbitMQ를 붙이는 중임. 클라이언트가 어디서 무엇을 읽을 수 있는지 제어하려고 JWT 인증을 쓰고 있고, 짧은 수명과 정기적인 토큰 갱신을 둠
    이 설정의 쉬움에 근접하는 다른 구성을 잘 모르겠음. 유효 세션에 JWT 토큰을 제공하는 엔드포인트를 하나 추가하면 끝이고, 사용자별 권한도 가능함

  • JWT를 쓰면 안 되는 이유를 설명한다는 링크 글 중 하나는 좋게 봐도 이상함
    https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba...
    요약하면 “일부 라이브러리에 버그가 있었다”이고, 그러고 나서 libsodium을 끌어와 직접 하라고 권함. 이건 진지하게 받아들이기 힘든 터무니없는 조언임. 모든 소프트웨어에는 버그가 있음. Heartbleed 때 인터넷 전체가 난리가 났지만, 우리는 여전히 TLS와 OpenSSL을 씀
    “JWT 명세는 특히 아주 짧은 수명 토큰, 대략 5분 이하만을 위해 설계되었다”는 말은 처음 듣고, 뒷받침할 근거도 찾을 수 없음. RFC 7519에는 그런 주장이 없음

  • 보통 JWT를 인증 캐시처럼 씀. 인증 서비스에서 인증 토큰을 받고, 그 토큰이 다른 서비스에 대한 권한을 부여함
    장점이 여러 가지 있는데, 핵심은 하위 서비스가 인증 데이터베이스와 상호작용할 필요도 없고 토큰을 발급할 권한을 가질 필요도 없다는 점임. HMAC이 아니라 RS256을 쓴다는 전제임. 따라서 하위 서비스가 침해되어도 인증 데이터베이스에 접근할 수 있는 서비스가 침해된 것만큼 치명적이지 않음
    토큰 안에 민감한 데이터가 있으면 JWE를 써야 하지만, 사용할 때마다 비공개 키를 가진 내부 서비스에 토큰 해독을 요청해야 해서 그다지 좋지는 않음
    내가 흔히 쓰는 구조는 {"id": (uuid), "scopes": ["scope:read/write"]}
    SPA에도 꽤 좋음. 정적 사이트 서버가 리소스를 제공하기 전에 공개 키로 JWE를 검증할 수 있기 때문임. 내 방식은 정적 사이트를 /(scope)/path 형태로 컴파일해 두고, 정적 서비스가 접근할 수 없는 페이지는 애초에 제공하지 않게 하는 것임. 관리 패널처럼 백엔드가 가진 기능이나 공격 가능한 내부 서비스 경로를 사용자에게 노출하고 싶지 않을 때 매우 유용함
    “백엔드 접근”용 JWT 수명은 약 5분이고, /me 같은 것은 /refresh에서 localStorage 캐시를 버리라고 명시하지 않는 한 localStorage에 캐시함. SPA 애플리케이션의 요청 핸들러가 “갱신 필요”를 감지하고 토큰을 갱신함
    이 책임의 대부분은 node/next와 Python 라이브러리에 있다고 생각함. 백엔드는 강타입 언어로 작성하고, 프론트엔드는 항상 미리 컴파일된 정적 페이지로 만듦. 현재 프론트엔드 구성은 VITE를 쓰며, 랜딩은 사전 렌더링 페이지로, 애플리케이션은 일반 SPA로 둠
    이 모든 걸 감안해도 이 gist 전체에는 강하게 동의하지 않음. JWT는 원하는 만큼 안전하게 만들 수 있음

  • JWT는 괜찮고, 제목이 좀 선정적으로 보임
    대신 이야기하기 좋은 주제들이 있음: 암호화된 값(대칭 또는 비대칭), 무작위지만 비밀인 값, 서명된 값(읽을 수는 있지만 변조할 수 없는 값)을 언제 써야 하는지, 이런 값을 어디에 둘지(메모리, localStorage, 쿠키), 값이 영원히 지속되지 않게 하는 방법과 자연 만료 시각 전에 폐기할 필요가 있는지 같은 것들임