そもそもJWTに関する私の理解は完全に間違っていた!

TL;DR

  • ステートレスなJWTはそもそもセッションの代替では無い
  • アクセストークンとしての利用が基本で数分レベルの短寿命な有効期限で利用
  • 従来のセッションに近い概念はリフレッシュトークン。てか、リフレッシュトークンはセッションでも(たぶん)良い
  • ユースケース的にモノリスには不要。SPAでMSAな時にメリットが出て来る。

はじめに

Webで認証システムといえばセッション! と言う感じのレガシーおじさんなのですが、最近はJWTとかも出てきてとても気になっていました。新しいものは使ってみたくなりますよね?

なので以前軽く調べてみたたのですが「JWTは危ない」とか「JWTをセッションに使うな」的な記事が大量に出て来ます。

co3k.org

qiita.com

一方で反論記事もあり色んな議論が渦巻いています。そもそも利用を推奨する記事も多い。

auth0.hatenablog.com

この辺りで「JWT怖い!」となり、状態を結局サーバで管理しないとセキュアに使えないならメリットは一体? とも思って一旦放置してたのですが「リフレッシュトークンとアクセストークンに分けて考えるのが前提」と言うのを下記の記事で理解してスッキリしました。

auth0.com

といわけで、私ので理解があってるかの確認がてらまとめてみました。

たぶん私と同じくユースケースが理解出来てなくてイメージが出来なかったり過った利用方法をする人も居ると思うので、そもそも何で必要なの? ってところを重点的に書いてみました。

実際にJWTをまだ使っては無いので理解が過ってるところがあれば指摘いただければと思います。

そもそもセッションにJWTを使って良いの?

「ダメです」

特に、JWTのメリットとして語られるサーバサイドで状態管理しなくて良いから云々でセッションの代替として使うのはアンチパターンと思われます。

そもそもセッションとは状態管理です。その時点でステートレスなインフラを作るのは難しくクライアントサイドでの管理はリスクがあります。幻想は捨てましょう。

ただし、JWTをアクセストークンとして使う事で今まではセッションで実現していた事の一部が代替できます。それは「認証」です。

そもそもセッションがなぜ必要?

さて、そもそもセッションはなぜ必要でしょうか? Webで言う「セッション」とはステートレスなプロトコルであるHTTPにサーバサイドで状態を持たせる仕組みの総称です。

元々は論文などのドキュメント公開の仕組みとして作られたHTMLとHTTPは状態が必要に無いのでその仕組みは存在しません。しかし、ひょんな事からWebアプリケーションとして様々な事に利用されるようになっため状態を持たないと都合が悪いケースが増えて来ました。

その流れでクライアントサイドに情報を保存するCookieなども登場し、クライアントとサーバで共通のキーを持ちそのキーにサーバサイドで情報を紐付ける「セッション」が実現できるようになりました。

f:id:pascal256:20191103131008p:plain

「セッション」を使う事でユーザがログインしているかを判別してユーザ毎の情報を表示するなどの状態をベースにしたWebアプリケーションが作れるようになりました。

セッションのストレージには色んなものが試されていて「NFS + ファイル」「RDB」「インメモリ」「KVS」とかありました。現在の主流はKVSでしょう。JavaEE系はインメモリも現役ですね。

歴史があるだけあってこのCookieとサーバサイドのストレージを使ったセッションはセキュリティ的にも運用的にもかなりの積み重ねがあります。そのため安心して使えるのですが問題ももちろんあります。

ステートレスアプリケーションとスケールアウト

サーバが3台とか5台くらいなら割となんでも良いのですが、サーバをスケールアウトして30台とか100台でWebアプリケーションを実現しようとすると、セッション用ストレージのインフラを作るのが結構骨です。

その流れで普及しだしたのがステートレスアプリケーション。RESTとかROAとセットで語られることが多かったと思います。

ざっくりと言えば「セッションを使ったアプリケーションはスケールさせるのが大変だから情報をクライアントに持たせて全部リクエスト毎に付与しよう」と言う考え方です。

これならサーバに状態を持たなくて良いからストレージがSPOFやボトルネックになる事も無いわけです。なので基本的な考え方になって行くわけですが、この考え方がだと送られてくるデータを全部信用するしか無いわけです。

そもそも渡されるu_idを自由にクライアントが指定できるのでなりすまし天国です。なので、バックエンドAPIとしての利用が限界でそれも「信頼されたアプリケーションから送られて来た情報は信頼する」と言うのが前提となっているので少し弱い。

マイクロサービスとSPA

色々あってマイクロサービスが流行ります。これをサーバサイドでアグリゲーションして一つのWebページとして返してる分にはあまり問題が無いのですが、SPAとかでクライアントサイドでアグリゲーションする場合は問題です。

流石にクライアントサイドからの情報を無条件に信用するステートレス実装はできません。なので、セッションなどを使ったステートフルな実装が基本になるかと思います。その場合はセッションのチェックが1ページ見るのに5個も10個も走る事になります。

今時はインメモリ実装かKVSなのでそこまで一つ一つは大きなコストでは無いですが無視できるものでも無いはずです。特にマイクロサービスだとインメモリは不可能なので必然KVSになります。負荷の集中もそうですが、マイクロサービスなのに共通のストレージにアクセスするのは正直筋の悪い設計となってしまいます。

f:id:pascal256:20191103133958p:plain

この辺はエアプなので想像ですが、たぶんこう言う問題は起こるはずです。

ソーシャル・ログインとAuthorizationプロトコル

少し違う文脈としてソーシャル・ログインの台等とそれを支える技術であるOpenIDから始まりOAuthを経てOIDCに辿り着いたAuthorizationプロトコル群があります。

以下のブログでも書きましたが、もはや自前で認証機能を作るのはリスクの観点で可能な限り避けるべき事なので、GoogleTwitterといった信頼できる認証システムをOAuthやOIDCで使うのが一般的です。 koduki.hatenablog.com

これらの認可プロトコルでは必然的に認可の基盤と実際に利用されるアプリケーションが異なります。その為ものすごく素朴に実装すると毎回認可サーバに問い合わせます。

これはインターネット越しだとそれなりのレスポンスになるはずなのでなんとかしたい。。。と言うわけでこの手のプロトコルの仕様を決める際に寿命の長いリフレッシュトークンと短いアクセストークンに分ける考え方やステートレスに認証できるJWTも作られた見たいです。

リフレッシュトークンとアクセストークンを利用した認証

さて、前置きがとても長くなったけどようやく本題のリフレッシュトークンとアクセストークンのお話。

マイクロサービスとSPAのところでした説明した図を思い出して欲しいのですが各サービスではクライアントの情報は信用できないのでサーバサイドで管理する必要があリマス。逆転の発想で「クライアントからのリクエストが信用できればサーバでの状態管理は不要」となります。

「ユーザID」や「どの機能アクセスできるか等の権限」といった情報を信頼できるサーバで秘密鍵により電子署名を付与する。APIサイドでは公開鍵を使って改ざん検知をするだけで渡された情報を信用できるのでストレージにセッション情報を保存する必要はない。これがアクセストークン。

では、信頼できるサーバとは何か? そもそもクライアントで秘密鍵を使って署名を付けてもオレオレ証明書なので信頼性はない。なので、第三者の認可サーバを使う。ここでユーザIDとパスワードだとかMFAだとか良い感じにユーザを認証してその証明となるキーを発行する。これがリフレッシュトークン。

リフレッシュトークンを持ってるクライアントが認可サーバに必要な情報を渡して署名付きのアクセストークンを作るという流れです。

f:id:pascal256:20191103154050p:plain

JWTは署名付きで任意のペイロードを持てるのでステートレスなアクセストークンの実装に最適という話。そしてリフレッシュトークンはステートフルが要求されるのでJWTを使う意味はあまりないはずです。

自分で実装するならリフレッシュトークンはトラディショナルなCookie + サーバサイドなセッションがWebアプリは一番楽かもと思っています。

JWTとセキュリティ

さてステートレスなJWTは実際セキュアだろうか? これは実装次第だけど基本的には問題ないと思います。

当たり前ですがJWTであるアクセストークンは改ざんチェックしかしてないので本人認証としては弱い。なんらかの問題で流出したらアウト。秘密鍵を変えるか有効期限が過ぎるのを待つしか無いです。

たとえリフレッシュトークンを使って再発行しても古いアクセストークンをステートレスに無効にすることは出来ない。やるならブラックリストデータを状態として持つしかない。

なので理想的にはアクセストークンはワンタイムトークンとして振る舞うのがセキュリティ的には良いと思います。仕様上それは出来ないから1分以下とか超短期の有効期限にするのがベスト。

これならアクセストークンが流出してもあまり問題はないはず。リフレッシュトークンはサーバサイドで状態を持ってるので万一流出したら破棄することが出来るので通常のセッションと同程度の長さで問題はないし明示的なログアウトも作れます。

ただし、アクセストークンの有効期限が短いと認可サーバへの問い合わせ回数が増えるのでここはトレードオフですね。認可サーバの負荷的には従来のセッションによると大差無い気もするけどクライアント側の通信コストが問題。ユーザが多いと署名コストもボトルネックになるかもです。

この辺のさじ加減は作ってみないと分からないのでバランスはサービス次第になるかと思います。

JWTはセッションの代わりになるのか?

なりません。

セッションが担っていた一部である認証に関しては限定条件で効果的に果たせそうです。ただ、そもそも状態の保存ができません。「ペイロードに格納すれば良いじゃん?」って話ですがそれは都度都度アクセストークン発行すれば出来ますが、たとえばAmazonの買い物カゴみたいなのをJWTで作るのはかなり面倒では。

その場合は大人しくCookieやLocal Storageに格納してAPIのBodyに入れるのが良いかと思います。暗号化もサポートしてるけど多少センシティブなものも突っ込めそうですが、ストレージとしての用途を考えて作ってるとは想定しづらいのでセッションの単純置き換えは無理でしょう。この点に関してはそう思ってる人も多分居ないと思いますが。

まとめ

さて、思ったより長かったけどJWTを使った認証の流れとユースケースを私なりにまとめてみました。

何というか従来的なモノリスなアプリケーションだと何ら意味が無いという事が分かった。

セキュアに作るならステートフルなリフレッシュトークンとステートレスなアクセストークンにする必要があり、単独アプリならステートフルなトークンすなわちセッションだけで十分だし。ソーシャルログインとか別の仕組みの一部として使う分には別だけど。

ここを理解しないとJWTをセッションに使おうという発想になってしまうのだろうなぁ。とは言え私の今の理解が正しいとも限らないけど。認証周りは本当にややこしい。。。

それではHappy Hacking!

参考