ディスカッション (11件)
近年、Web認証のデファクトスタンダードのように扱われてきたJWT(JSON Web Token)ですが、その扱いには慎重になる必要があります。なぜJWTを使い続けることがリスクになり得るのか、その理由と代替案について議論しましょう。
補足が必要ですね。ブラウザベースのユーザーセッションに限った話です。
サービス間通信でJWTを使うなら、いくらでも良い使い道はありますよ。
追記:リンク先の記事( https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid )をいくつか読みました。もしJWTがそこまで恐ろしくセキュアでない標準規格だと言うのなら、AWS STSのAssumeRoleWithWebIdentityをハックする方法をぜひ公開してほしいですね。あるいは公開せずに、フォーチュン500企業のAWS本番アカウントすべてで暗号通貨マイナーを動かして悪用してもいいんですよ。JWTはそんなに危険なんですよね?いつか必然的に成功した時にでも教えてくださいよ。……皮肉ですよ。
この記事の大半はその根拠として他のブログにリンクを貼ってるだけみたいですね。そのリンク先も「JWTは個別に無効化できない」という点に文句を言っているようですが、僕が実装してきた中では、どこかで無効化されたnonce(ナンス)をチェックするっていうのが一般的なガイドラインですよ。これでそのブログの2つ目の指摘も解決しますしね。
JWTの仕様自体、セキュリティ専門家から信頼されていない
これ、たった一つのブログ記事以上の根拠が必要な気がします。しかもそのブログ記事も、大半は不適切な実装のせいにしているだけじゃないですか?どんな規格でも付きまとう問題でしょう。
結局、適当なgistのリンクをクリックして何を期待していたのか自分でもよくわかりません。
JWTは危険って言うけど……RSA/PPKベースの信頼できる署名方式を使ってもですか?共有秘密鍵じゃなくてですよ。
JWTは有効期限が長すぎる、という意見もありますが、JWTの寿命を制限して認証局に対してリフレッシュモデルを実装すればいいだけの話です。Cookieベースのセッションだってどこかに保存するわけでしょう。JWTなら有効期限を5〜15分に設定できます。15分というのはEntraなど多くの認可システムでのキャッシュ時間とほぼ同じですし、リフレッシュシステムがあれば5分トークンでもブラウザから問題なく使えます。
最後に、自分はアイデンティティや認証をアプリケーションやAPIサービスから分離する方が好きですね。コンテキストを外部化できるし、リクエストごとにJWTを扱う方が、断続的に失敗する可能性のある共有キャッシュや状態管理システムを介するよりもずっと楽ですよ。既知の認証局に対して署名を検証できるわけですから。
今ちょうどウェブサイトの通知プッシュ機能にRabbitMQを追加しているところです。どこで何を読むことがクライアントに許可されているかを制御するためにJWT認証を使っていて、短い有効期限でトークンを定期的にリフレッシュしています。
これほど簡単にセットアップできる構成は他にないですね。有効なセッションにJWTトークンを発行するエンドポイントを一つ作るだけ。しかもユーザーごとの権限管理付きです。
セッションとJWT無効化リストの比較ですが、JWT無効化リストに軍配が上がる理由があります。JWTには期限があるため、期限切れになっていないトークン用のリストだけを管理すればいいんです。実際には有効なJWTに比べて無効化されるのはごく一部なので、リクエストごとに参照すべきデータセットは非常に小さくて済みます。
セッションを使う場合、有効なセッションのリストは無効化リストよりも桁違いに大きくなるはずです。そうなるとデータ検索のコストや状態管理のストレージコストは高くなります。
それに、記事ではJWTはステートレスだと言っていますが、それは通常正しくありません。ほとんどの場合、JWTの検証だけでなく、ユーザーがまだ有効かを確認するためにリクエストごとに一致するアイデンティティオブジェクト(ユーザー詳細など)を取得するでしょう。ユーザーごとの無効化リストや、JWTのiatフィールドを検証するminimum_issued_atなどを活用すればいいんです。これなら「全デバイスからログアウト」というパターンも、ユーザーのminimum_issued_atフィールドを現在時刻に設定するだけで実現できます。個別に無効化チェックをしなくても、以前のトークンはすべて無効になります。
偶然この記事を見つけたんですが、過去にかなり取り組んだトピックが今トレンドになっているのが興味深いですね。クリックして読み進めたら、筆者が自分の過去の成果物を引用していて驚きました!懐かしいですね。
とにかく、自分より賢い人たちが長年にわたってこのトピックを掘り下げてきましたが、それでも2026年の今でも、Web認証にJWTを使うのは間違った選択だと思います。サービス間通信ならいいですが、もし選択肢があるならPASETOを使うべきですよ。あれなら多くの問題を解決できますから!
んー、自分はJWTを「認証キャッシュ」として使うことが多いですね。認証サービスからトークンを取得して、それを使って他のサービスへのアクセス許可を得るというやり方です。
これにはメリットがいくつかあって、最大の利点は、サブサービス側が認証データベースとやり取りしたり、トークン発行権限を持ったりする必要がないことです(HMACではなくRS256を使う前提ですが)。だからサブサービスが侵害されても、認証DBにアクセスできるサービスがやられるほど壊滅的なことにはなりません。
もしトークン内に機密データを入れるならJWEを使うべきですが、毎回トークンをデコードするために(秘密鍵を持つ)内部サービスに問い合わせる必要があるため、使い勝手はあまり良くありません。
自分の典型的な構成は {"id": (uuid), "scopes": ["scope:read/write"]} という感じですね。
あとSPAにも便利です。静的サイトのサーバーが、リソースを提供する前に公開鍵でJWEを検証できますから。自分の場合は、静的サイトを /(scope)/path の形式でビルドしておいて、アクセス権限がないパスにはそもそもページを返さないようにしています。バックエンドが持っている権限や、攻撃されそうな内部サービスのパスをユーザーに晒したくない管理パネルには非常に役立ちます。
JWTの寿命はバックエンドアクセス用で5分程度にしています。「/me」みたいなデータはlocalStorageにキャッシュしますが、/refreshで明示的に破棄するようにしています。SPAのリクエストハンドラで「リフレッシュが必要」と判断したらトークンを更新する仕組みです。
この記事の不満のほとんどはNode/NextやPythonのライブラリに原因がある気がします。自分はバックエンドを静的型付け言語で書いて、フロントエンドは常に事前ビルドした静的ページで作っています。今のフロントエンドのセットアップはVITEを使っていて、ランディングページはプリレンダリング、アプリケーション部分は通常のSPAという構成です。
というわけで、このgistの内容には全く同意できませんね。JWTは使い方次第でいくらでもセキュアになりますよ。
サーバー間通信でJWTをいくつか使っていますが、その用途においてJWTは最高ですよ。でもフロントエンドのチームメンバーには、絶対に使うなと言い続けてきました。
JWTが登場した頃からこの沼にはまっていますが、他の方も言っている通り、CookieにはCookieのリスクがあります。今はXSSとCSRFの両方に気を配らないといけないし、SameSiteやトークンといったCSRF対策は、同一オリジン攻撃であるXSSに対しては無力です。
はっきりさせておくと、httpOnlyやsameSiteがXSS下でlocalStorageのように無力というわけではありません。XSSはhttpOnly Cookieを読み取れないので認証情報を盗み出すことはできず、被害者のブラウザでセッション中に攻撃を実行することしかできません。localStorageにあるJWTなら、オフラインで有効期限中ずっと使い回されてしまいますからね。あとこれも区別しておきたいんですが、脆弱なのはJWTではなくlocalStorageの方です。とにかく、頼むからJWTをhttpOnly Cookieに保存するのはやめてください。
「JWTを使うのはやめて、俺が作った同じ問題を抱えたJWTのコピーを使え!しかも今回は独自の雪の結晶(snowflake)シリアル化フォーマット付きだ!」ってか。
笑わせますね。
JWTは、まともなアルゴリズムさえ使っていれば何の問題もありません。無効化の問題がないわけでもないですし、無効化ポリシーをレプリケートすれば済む話です。例えば、無効化トークンの直接リストを保持するか、「このタイムスタンプ以前のトークンは信頼しない」といった一括管理を行えばいいだけです。