結局、JWTって使っていいんだっけ?
SPAのセッションの維持(特にログイン状態の維持)において、JWTを始めとするトークンを用いるのはいいのか改めて疑問になったので、軽く得られた知見と意見を書かせていただきます。以前の私は、SPA等のWebアプリケーションの作成チュートリアルなどをよく見ていましたが、ログイン維持(フロントエンドからリソースサーバへのAPIリクエスト)には、JWTを使って認可してもらいましょうという方法を数多く目にし、そのうち脳死でJWT便利!万歳!という風に盲信していました。以前に携わっていた企業のプロジェクトでもJWTをセッション管理(主に認証状態の維持)に用いる場面が多く、とくになにも疑問を思っておりませんでしたが、果たしてこれがベストなのかという疑問を抱くようになりWebで調査を行いました。
基礎知識
HTTPとセッション
すみません、こちらの歴史等については認識が曖昧なので他の記事等を併用していただけますと幸いです。
HTTPはもともと論文公開等の用途で使われた歴史があり(どこかで読んだ)、現在あるようなショッピングサイトなどユーザの状態・行動などを追跡する機能は備えていません(そもそもHTTPにセッションについての規定がない。)。しかし、HTTPのプロトコルを用いてユーザの状態を追跡するためにセッションを管理する方法が導入されました。セッション(一連の処理の開始から終了までを表す概念)を管理(維持)にするにあたり、サーバ側でSessionIDを発行し、ブラウザのCookieを代表するものでSessionID(またはそれに関連づけられるデータ)を保存し、HTTPリクエストのたびにCookieに紐づけられたSessionIDを送信することでサーバ側は同一のセッションと判断し、ステートフルな通信・UXを実現できるようになりました。(具体例としては、ログイン状態の維持、ECサイトでのカート情報の維持など。)
今回の記事では、SPAにおけるセッション管理をJWT(ステートレスと扱われてきた(後述))で行う方法とSessionIDで行う方法がある中でそれぞれのメリット・デメリットと回避策を記載いたします。
JWTとは
詳しくは説明しません。
定義
RFC上の定義に沿うと、
- HTTPヘッダーやクエリパラメータにJSONデータをURLセーフかつコンパクトに載せられるようにしたもの。
と表現できます。- JSONデータをBASE64URLエンコードする
- 多用されるJSONキーをコンパクトにする
構造
ヘッダー、ペイロード、署名がドットで繋がったものです。
ヘッダー
ハッシュアルゴリズム(alg、署名用)、とタイプ
{
"alg": "HS256",
"typ": "JWT"
}
ペイロード
いわゆる、データ本体。(iss,sub,sud,expなどといった予約済みクレームがある(RFC参照。))
{
"iss": "hikarucraft.com",
"sub": "samples",
"exp": 1670085336
}
予約されているクレームだけでなく、任意のクレームも追加可能。
{
"iss": "hikarucraft.com",
"sub": "samples",
"exp": 1670085336,
"email": "info@hikarucraft.com",
"role": "admin"
}
署名
ヘッダーとペイロードをそれぞれBASE64URLでエンコードし、出力を秘密鍵で署名したもの。
セッション管理方法の比較
SPAでログイン画面を通してユーザ認証を行ったあと、どのようにセッション管理するかという方法についてです。
Session IDの発行
認証後に、サーバ側でSession IDを発行し、クライアントにCookieとしてIDを保存する方法です。
メリット
- Session IDやパスワードの漏洩、パスワードの変更後に既存のセッションをすぐにログアウトできる。
デメリット(よく議論にあがること)
- セッションIDをDBに保存するので、スケーラビリティに対応しずらい(DBのスケールアウトは可能になってきているのでは?)
- サーバがリクエストごとにセッションの状態を確認するので高トラフィック環境ではボトルネックになる。
- セッションタイムアウト後の再認証が必要。
- Session IDを推測されないような対策が必要。
運用する時の注意点(セキュリティ対策)
- CSRF対策
- Cookieの属性を適切に設定する。(secure属性、HttpOnly属性、SameSite属性)
- Session IDは推測不可能なものにする。(今後別記事で記載します)
トークン(主にJWT)の発行
メリット
- Session IDを持たず、リクエストごとにDBにアクセスしないのでスケーラビリティに適している。
- SPAなどでは1ページ表示するためにも、場合によっては多くのバックエンドのエンドポイントにアクセスするため、バックエンドでJWTを検証するだけで済むのは効率がいい。
- PostgreSQLのRLSなどを用いたマルチテナントサービスを作る場合、JWTのペイロードからテナントIDを引っ張ることで、RLSで保護されていないテーブルにアクセスする必要がないセッション管理にJWTを使うと何が変わるの?
デメリット
- セッションの即時無効化が不可能。
運用する時の注意点(セキュリティ対策)
- アクセストークン(リソースサーバにアクセスする時のトークン)の有効期限を短くする。そのかわり、リフレッシュトークン(ユーザに紐づいてサーバ側でDBに保存する)を用いてアクセストークンを再生成するようにする。(chatworkの事例)
- リフレッシュトークンを用いる場合は、ユーザの安全確保のために、リフレッシュトークンをブラックリストに入れる運用が望ましい。
- サーバサイドでヘッダーのバリデーションを行う(こちらが参考になります。)
- PR TIMESの事例が参考になる(https://developers.prtimes.jp/2023/12/26/implemented-login-system-using-jwt/)
- JWTはステートレスの運用だけに使わなくてもいい。というのは、JWTの武装のように、Session IDやそれに紐づく識別子をJWTのペイロードに含ませて、JWTの無効化をサーバでできるようにするということ。(たぶん、ペイロードのjtiの活用)
意見諸々
感想
この記事を書くにあたり、調査を始めた頃、JWT運用に否定的な意見が多く、彼らの主張するJWTのデメリットの根拠にも納得していたので、従来からのSession IDでのセッション管理がいいのではないかと思っていました。しかしながら、JWT(ステートレスなトークン)にもメリットがありセキュリティ対策を施すことで便利な面が多くあることがわかったのでJWT運用に否定的ではありません。第1に、要件定義次第ですが、セッション管理は方法を使い分けることが必要でしょう。第2に、JWTというのはあくまでもエンドポイント間のデータ通信のフォーマットにすぎず、ステートレスである必要もないので、サーバ側で保存しているSession IDとの併用もいいのではないかと思います。JWTが認証やセッション管理の固まった方法ではないというのが学びです。JWTのペイロードにはカスタムに属性を含めることができるので独自のアプリ設計でカスタムしていくことも一つのアプローチだと思います。
いずれにせよ、よくわからないで使うのはNGです。
コメントお待ちしております。
参考資料
- https://developer.mamezou-tech.com/blogs/2022/12/08/jwt-auth/
- https://developers.prtimes.jp/2023/12/26/implemented-login-system-using-jwt/
- https://kurochan-note.hatenablog.jp/entry/2022/04/18/112307
- https://speakerdeck.com/defunty/spadefalseren-zheng-fang-fa-niguan-surumasakaribuntou-gehui-chang-hakotiradesu
- https://www.docswell.com/s/ockeghem/ZM6VNK-phpconf2021-spa-security
- https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/
- https://koduki.hatenablog.com/entry/2019/11/03/163014
Discussion