"JWT=ステートレス"から一歩踏み出すための考え方
おはようございます、ritouです。
この話に乗っかっていきます。
3行で
- ログアウト時にJWTを無効化できない実装は今後脆弱性診断で「OWASP Top 10 2021違反」と指摘されるようになりそう(今も個別にされてるかもしれないけど)
- JWTは単純なフォーマットなので、ステートレスなセッション管理においてログアウトしたときに文字列自体を無効化できない件は独自エンコード方式(一般的にフレームワークのCookieストアと呼ばれているもの)でも起こり得る
- 「セッションID vs JWTで内包」 以外にも 「セッションIDをJWTに内包」もあり得る。既存の機能を残しつつ「JWTで武装」する選択肢も考えてみてはどうか。
ステートレスなセッション管理でログアウトの際に文字列自体を無効化できない問題
これは前から言われていますし、駆け出し何とか勢のQiita記事に書かれるぐらいには一般的です。
2021年にもなって SPAならJWT ぐらいにしか思ってない人には何もいうことはございませんが、JWTはあくまで署名や暗号化可能なエンコードフォーマットなのでこの問題を整理すると、
- まぁ昔からCookieストア方式のセッション管理ってのは存在するし特徴を知っててリスク受容できていれば問題ないだろう
- アクセス頻度などをチェックして定期的に有効期限を適切に伸ばしたJWTを発行していくなど、リスクを減らすワークアラウンドもある
- JWTでステートフル言ってるうるせぇ猫野郎がいるな
みたいな認識だったところが、
- ログアウト時にJWTを無効化できない奴は何をやってもダメ
ってなってザワついちゃうんじゃないかいうお話です。
今回の話は "Cookieストア" な仕組み全般に言えるかもしれない
この問題を 無駄に大きく捉えると、 JWTだから!って話ではなく、Webアプリケーションの "Cookieストア" 相当の実装でログアウト時にHTTP Cookieを削除するだけ の実装も対象に含まれるのではないかとも考えています。
- ログアウト機能はあるが直前に利用できていたセッションCookieの値が再利用できる
みたいな実装になっていたら、原理としては無効化できないJWTと一緒です。
ちなみに、JWTを無効化する方法として、徳丸さん、川崎さんTweetにある通り、
- 無効化対象のjtiを管理しておく
みたいなアイディアは昔からあります。
OAuth 2.0のAccess TokenへのJSON Web Token(JSON Web Signature)の適用 - r-weblife
(この記事は2014年のものですが、それより前の有識者の方の記事を参考にしたものなので、アイディア自体はとっても古くからあります。)
とはいえ、、 そこまでするならJWT使わないだろwww というのがこれまでの多数の意見なのではないでしょうか。
「セッションIDをJWTに内包」という考え方
いわゆる「セッションID vs セッションデータ内包」なイメージができている場合、今回の話を受けて、 普通にセッション管理でいいじゃん となる人もいるでしょう。
この意見に異論はありません。
SPAでもこのいわゆるセッションIDの値を何とかストレージとかお好きなものに保存してやっていけば良いのです。
その形式のリクエストを処理するミドルウェアがないのであれば、そのフレームワークでは今まで想定されていなかった、それだけのことでしょう。
しかし、個人的には、ここから一歩踏み出した考えを持っていただく絶好の機会だと思うので少し追記します。
「セッションIDをJWTに内包する」 という考え方です。
セッションIDを内包したJWTをセッションCookieとして利用することで
- セッションIDを用いたセッション管理機能はそのまま利用できる
- 署名の仕組みを用いて改竄検知が可能
- 内部でセッションIDを無効化することでJWT自体も無効化可能
- JWTの有効期限を更新していくことで「一定期間で無効になる文字列」をゆるく実装可能
という状態になります。
私はこれを「JWTで武装」のように表現することがあります。
後述する資料にて、OAuth 2.0のアクセストークンへのJWT適用例の一つとしてハイブリッド型アクセストークンについての記事を紹介したりしています。
単一のWebアプリケーションのセッション管理を強化、なんて言ってもそんなに厳しい要件ないから良いや、ぐらいに思われるかもしれませんが、これが複数のリクエストを繋げて実現するmicroservices的な構成であったらどうでしょうか。
- 明らかに無効な文字列 "hogehoge"
- JWTのPayloadにあるセッションIDを改竄したもの
- 有効期限が切れたJWT文字列
などを、データストアを参照する前に弾くことができるので、無駄なリクエストを減らすこともできるでしょう。
もちろん要件に対してオーバースペックな実装は不要だと思いますが、この辺りの感覚を持っていることで今後の設計/実装に活かすこともできるでしょう。
参考資料
今回の話含めてJWTの活用について、この記事にまとめています。
良かったら読んでみてください。
2020年版 チーム内勉強会資料その1 : JSON Web Token - r-weblife
そして、このCookieとの比較でもよく言及される点として、「JWT=ステートレスでなければならない」「JWTを使う場合は必要な情報を詰め込み、その内容だけで機能を実現させる必要がある」という一種の信仰のようなものがあるのではないかと考えています。
これはとても勿体無いなと感じていて、確かに情報を内包できるという特性は持っていますが、データストアなどを参照するためのキーを持ってはいけないというわけではありません。
例えば単純な識別子を用いたやりとりだったものにJWTを適用するだけで、有効期限や検証という機能を追加できる のです。この考えは既に色々なユースケースで使われているものではありますが、ステートフルなユースケースへも適用できるということを今一度意識してもらえると良いのかなと思っています。
ではまた!
Discussion
こちらの記事を非常に興味深く拝見させて頂きました。一点気になったことがあるので質問させてください!
記事中の"内部でセッションIDを無効化することでJWT自体も無効化可能"というのは具体的にどういった操作になるのでしょうか?
セッションIDに紐付いたログイン情報を保存しているデータストアで無効化フラグをつけるわけではなさそうなので、JWTを意図的に改ざんするとかなのかなと思いましたが、それだと攻撃者によって改ざん前にJWTがコピーされていた場合に無力だなと思いました。ご教授頂けると幸いです。
多分もう見ていないと思いますし、私が答える事でもないと思いますが、似たことを考えて実装をしたことがあるので…
この点については文字通りの意味だと思います。
まず、今まで通りのセッションIDを考えてください。
サーバサイドでデータストア(RDBMS,Memcache,Redis等)でセッションIDとユーザを紐づけることをやっていると思います。
なので、セッションIDをデータストア上から削除すれば無効化できますよね。
JWTは「署名をつけることもできる規格化された入れ物」です。なのでJWTの中にセッションIDを持つことは可能です。
JWTからセッションIDを取り出してデータストアに突き合せて使うことができます。そのセッションID自体は上記の操作で無効化できます。ただそれだけだと思います。
わざわざJWTで包むと何がうれしいのかというと、書かれている通りなんですが、JWT(正確にはJWSによる署名を含む)により「(署名の鍵が盗まれていない限り)JWTを発行した人が信頼できること」「内容の改ざんがないこと」「JWTによる期限」といったことが検証できます。
これらはデータストアに問い合わせることなく可能なので、場合によっては負荷が軽減します。署名検証分の負荷は増えますが、ネットワークレイテンシと比べたら些細です。また、悪意あるユーザからの攻撃に対してデータストアの負荷が軽減するといった効果があります。(セッションIDだけではとりあえずデータストアに突き合せないといけないですからね。)
また、セッションIDは作り方が課題です。
セッションIDには乱数が用いられたりしますが、脆弱な実装だと乱数を推測できたり固定化することができてしまいます。特にOSSなフレームワークを使っていると分かると突かれることがあります。
極端な話ですがセッションIDを適当に作っていたらログイン出来ちゃったみたいなことも考えられます。
(もちろん、今はほとんどフレームワークが強固な方法で用意してくれており、メジャーな対策は含めているはずなので気にすることはあまりないかもしれません)
そんなセッションIDをJWTで包んでやると署名がつくので、HMAC等の署名技術が破られない限りそういった攻撃は困難です。
(署名も適当に作ったら通っちゃったみたいなケースは考えられますが、セッションIDと署名を同時に当てるのはセッションIDをあてるよりも絶対に困難だと思います。もちろん署名の作り方にもよりますが脆弱な方法を使わないといった一般的な話として・・・)
その通りです。
これはセッションハイジャックであり、セッションIDだろうがJWTで包もうが、秘匿されるべき情報が盗まれてはどうしようもありません。
これに耐性がある仕組みは存在しないので、セッションハイジャックは起きないように実装すべきでしょう。
なお、JWTに利用者のIPv4を埋め込むことで、通信者のIPとJWT内のIPが一致しないと通さないといった実装もできますが、IPは変わりますからセッション管理に使うには厳しいでしょう(他のユースケースでは使えるかもしれません)
ryozi_tn さん、コメントありがとうございます。
本件、質問者には別ブログの方で同様の質問があり回答していたので放置していました。
回答していただいた内容についても、認識の違いはございません。ありがとうございます!
そうだったんですね!お恥ずかしい…
見ていただきありがとうございました!