JWTの使用をやめよう
(gist.github.com/samsch)- JWTはユーザーをログイン状態に保つ用途には適しておらず、この目的には通常のCookieセッションのほうが適している
- JWT仕様はおおむね5分以下の短命トークンを前提としており、セッションにはそれより長い寿命が必要である
- 安全なステートレス認証を実現するのは難しく、トークンを安全に扱うには結局ある程度の状態保存が必要になる
- 単純なセッショントークンだけを入れたJWTは、通常のセッションクッキーより非効率で柔軟性も低く、認証情報はlocalStorageやsessionStorageに保存すべきではない
- 短命の署名付きトークンが必要な場合は、セキュリティを考慮して設計されたPASETOのほうが良い選択だが、セッション用途に使うべきではない
要点まとめ
- JWTはユーザーをログイン状態に保つために使うべきではなく、その目的には通常のCookieセッションのほうが適した手段である
- JWTはこの目的のために設計されておらず、安全でもなく、ログインセッションの維持には標準的なCookieセッションのほうが適している
- 関連事項として、JWTトークンを含む認証資格情報はlocalStorageやsessionStorageに保存すべきではない
- 関連する発表を見ることはできるが、CSRF保護のような他のテーマは概ね簡単にしか触れられていないため、別の情報源で別途学ぶ必要がある
- 動画の終盤で挙げられる「有効な」JWTのユースケースも、より良く安全なツールで簡単に処理でき、その具体例がPASETOである
JWTを避けるべき理由
- JWT仕様は約5分以下の非常に短命なトークンだけを対象に設計されており、セッションにはそれより長い寿命が必要である
- 安全なステートレス認証は不可能であり、トークンを安全に処理するには何らかの状態が必ず必要になる
- データストアが必要なら、一部のトークン状態だけを扱うより、すべてのデータを保存したほうがよい
- 関連する問題は http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/ でより詳しく扱われている
- 実際にJWTをこのように使うアプリケーションは存在するが、そのようなアプリケーションには欠陥があるため、同じ誤りを繰り返すべきではない
- 単純なセッショントークンだけを保存するJWTは、通常のセッションクッキーより非効率で柔軟性に欠け、追加の利点も得られない
- JWT仕様自体がセキュリティ専門家から信頼されていないため、セキュリティや認証に関わる用途全般から排除すべきである
- 元の仕様では偽のトークンを作成できてしまい、他にも誤りが含まれている可能性がある
- JWT系仕様の問題は JWT: The JSON Web Token standard is bad and everyone should avoid it でより深く扱われている
反論
- 「GoogleもJWTを使っている」という反論は、ブラウザのユーザーセッションには当てはまらない
- GoogleはブラウザのユーザーセッションにJWTを使わず、通常のCookieセッションを使っている
- JWTは、あるサーバーまたはホストのログインセッションを別のサーバーまたはホストのセッションへ渡すSingle Sign Onの転送手段としてのみ使われている
- この使い方はJWTの妥当なユースケースの範囲に入る
- Googleはより安全なJWT実装を作り維持するためのセキュリティ専門家のリソースを持っている
- GoogleのJWTは、実質的に他所のJWTと同じものではない
- 「ステートレスのほうが優れている」という反論は、安全な認証の要件と一致しない
- 莫大なリソースなしに真にステートレスな認証を安全に運用することはできない
- 関連する議論として Stateless is a lie を参照できる
- 「セッションの設定方法が分からない」という問題は、ほとんどのWebサーバーフレームワークのドキュメントと実装で解決できる
- セッション技術は特に新しいものではないため、セッションを説明する記事を頻繁に見かけるわけではない
- セッション実装のドキュメントだけでも設定手順をたどれるはずである
- ほぼすべてのWebサーバーフレームワークにはセッション実装が含まれており、デフォルトで有効でなくても通常は簡単に有効化できる
- Expressや他のNode.jsフレームワークは、高いモジュール性と単一目的の性格のため、ある程度例外に近い
- Expressでは
express-sessionミドルウェアと、保存先に合ったstore connectorを使えばよい - Postgres、MySQL、または可能ならSQLiteとともに
connect-session-knexの利用が推奨される
短期トークン
- 何らかの用途で短命の署名付きトークンが必要なら、セキュリティを考慮して設計された PASETO という、より優れた仕様がある
- PASETOを使うとしても、セッション用途に使うべきではない
セッションの仕組み
- セッションの仕組みをさらに学ぶには、joepie91のgist を確認するのがよい
2件のコメント
JWTはトークン暗号化とDBクエリ削減のための方式であり、クッキー認証と対立する概念ではありません。JWTをsecureクッキーに保存すれば、盗難リスクはレガシーなクッキー認証方式と同じです。
JWTをexpiredにするために失効リストを管理することには、性能面で利点があります。失効情報だけをredisで問い合わせるのと、全会員をDBに問い合わせるのとではコスト差があります。
数万回の頻度で10万会員rowに対してインデックスベースのクエリを行う(レガシーなクッキー方式)
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ライブラリもようやく最近になって概ねまともになったが、以前はそうではなかった。
"none"アルゴリズムを受け入れるライブラリも多かったし [1]、公開鍵を共有秘密のように使って攻撃者がトークンを偽造できるようにしていた例もあった [2]。これはリンク先の記事が批判している複雑さがそのまま生み出した結果だJWTはユーザーセッションで求められる機能を満たせないこともある。どこかに失効リストを置かなければ無効化できない。だが、毎回のリクエストで識別子を失効リストと照合する必要があるなら、いっそ不透明なセッションIDを使って毎回参照すればよい。もちろん短命トークンを使って継続的に更新することはできるが、どうせ状態を持つ必要がある普通のアプリケーションでそこまでする理由は薄い
ただし、分散システムやマシン間通信では署名付きトークンが有用な場合があるという点には全面的に同意する。この2つのケースを混同してはいけない
[1] https://nvd.nist.gov/vuln/detail/cve-2022-23540
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-54150
今では各言語の主要ライブラリがよりまともなデフォルトを備えるようになり、最近は実際かなり安全になったと見ている
「正しく扱えば安全だ」がそのまま良い設計を意味するなら、X.509のようなものにも同じことが言えてしまう
多くの場合にはもっと良い代替がある。標準的なセッショントークンやAPIキーはほとんどの大規模Webサイトで広く使われており、大半の用途にほぼ完璧に合っている
こうした標準にまったく価値がないと言いたいわけではない。最大の利点は、ASN.1エンコーディングのようなものなしに何かをやり取りするための基本標準である点で、ASN.1系のツールは非常に脆弱でバグも多いように見える
たとえばSAMLをどう悪用するかは知らないが、XMLパーサ全体を攻撃対象領域にしてしまうのでひどい標準だということは分かる。私はセキュリティ研究者ではないのでXMLパーサの脆弱性の見つけ方は知らないが、攻撃面が広いのは良くないことだとは分かる
JWTが安全でないというのは、信頼されたRSA/公開鍵ベースの署名方式を使っていてもそうだということか? 共有秘密でなくても?
JWTの寿命が長すぎるという主張も奇妙だ。JWTの有効期間を制限し、認証機関に対して更新モデルを設ければよい。Cookieベースのセッションを使うとしても、結局はどこかに保存している。JWTを5〜15分だけ有効にすることはできるし、15分はEntraを含む多くの認可システムのキャッシュ時間と近い。5分トークンでも更新システムがあればブラウザで十分使える
最後に、私はアイデンティティ/認証をアプリケーション・APIサービスから分離する方を好む。コンテキストを外部化できるし、リクエストごとにJWTを処理する方式の方が、ときどき失敗する共有キャッシュ/状態システムより扱いやすい。署名付きトークンは既知の機関に対して署名を検証できる
それ以外については署名は暗号学的に妥当だ。短命にして、すべてのJWTを毎回検証すればよい
参考までに、OIDCトークンはすべてJWTである
セッションとJWT失効リストを比べると、JWT失効リストに有利な理屈もある。JWTには限定された有効期限があるので、まだ期限切れになっていないトークンについてだけ失効リストを維持すればよい
流通している有効なJWTに比べれば失効済みJWTは一部である可能性が高いので、リクエストごとに参照するデータセットはごく小さくて済む
セッションを使う場合、有効セッション一覧は失効リストより何桁も大きい可能性が高く、そのため状態保存に伴う参照コストと保存コストも大きくなる
しかも記事ではJWTがステートレスだとしているが、普通はそうではない。たいていはJWTを検証するだけでなく、各リクエストで対応するIDオブジェクト、つまりユーザー詳細を取得して、そのユーザーがまだ有効か、当該操作を行う権限があるかを確認する。ユーザーごとの失効リストや
minimum_issued_atのような値を使ってJWTのiatフィールドを検証することもできる。こうすれば「すべてのデバイスからログアウト」のパターンも可能で、その操作ではユーザーのminimum_issued_atを$NOWに設定するだけで以前のトークンをすべて失効させられる。個別の失効リスト照会は不要だselect1回で、0〜1行を返すだけだ。大半の場合、心配するほどのことではないこの記事は「なぜ」に当たる部分の大半を別のブログ記事へのリンクに頼っているが、そのブログ記事は概して「個々のJWTトークンを無効化できない」ことへの不満を述べているように見える。
私が実装するときは毎回、どこかで無効化されたnonceを確認するのが一般的な指針だったので、それでその記事の2つ目の主張も解決する。
「JWT仕様そのものをセキュリティ専門家が信頼していない」という話は、ブログ記事1本以上の根拠が必要に思える。そしてその記事は概して悪い実装を責めているように見えるが、どんな標準でも悪い実装の問題はついて回る。
全体として、適当な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キー漏えいのほうがはるかに起こりやすい。
この話題をたまたま見かけて、以前このテーマでかなり作業していたので、また話題になっているのが興味深いと思った。ところがクリックしてみると、著者が私の資料の一部にリンクしていた。本当に懐かしい記憶がよみがえる。
ともあれ、私よりずっと賢い人たちがこの話題を何年にもわたって幅広く扱ってきたが、2026年になってもJWTはWeb認証には不向きな道具だと思う。サービス間用途なら問題ないが、選択肢があるなら単にPASETOを使うのがよい。多くの問題を解決してくれる。
https://www.paseto.io/
今、Webサイトに通知プッシュ用としてRabbitMQをつないでいる。クライアントがどこで何を読めるかを制御するためにJWT認証を使っており、短い有効期間と定期的なトークン更新を設けている。
この構成の手軽さに近い別構成をあまり思いつかない。有効なセッションに対してJWTトークンを提供するエンドポイントを1つ追加するだけで済み、ユーザーごとの権限設定も可能だ。
JWTを使うべきでない理由を説明するというリンク先の記事の1つは、好意的に見ても奇妙だ。
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、クッキー)、値が永続しないようにする方法、自然な有効期限の前に破棄する必要があるか、といったことだ。