「デバイスをまたいでもログイン状態を維持したい」という要件が来たとする。JWT やセッション ID といったセッション情報を、さてどこにしまっておくか。localStorage、sessionStorage、Cookie と選択肢はいくつもあり、それぞれに一長一短がある。ところが、セキュリティと運用のしやすさまで勘案すると、正解は案外あっさり絞られてくる。
そこで、代表的な四つの選択肢を比べながら、なぜ最終的に一つの方法に落ち着くのかを考えてみた。セッション情報は、いわばホテルの部屋の鍵である。どこに置くのか、誰が無効にできるのか。その二点で眺めると、話は存外すっきりする。
四つの選択肢
まず、localStorage に JWT を置く方法である。ブラウザを閉じても値は残るので、永続性の点では申し分ない。しかし JavaScript から中身が丸見えなので、XSS(クロスサイトスクリプティング)にはめっぽう弱い。
悪意のあるスクリプトが一度でも実行されれば、その瞬間にトークンは持ち去られる。鍵を廊下の植木鉢の下に隠しておくようなものだ。セキュリティを気にするなら、真っ先に候補から外れる代物である。
次に、httpOnly クッキーに JWT を入れる方法がある。Cookie の有効期限内なら保持され、JavaScript からは読めないので XSS への耐性もある。ここまではよい。
問題は運用だ。JWT はサーバー側に状態を持たないため、「特定のデバイスだけ強制ログアウトさせる」といった制御が難しい。フロントの台帳に載っていない合鍵を、客が持ち歩いているのに近い。かといってブラックリストで失効を管理しはじめると、ステートレスという JWT の売りがみるみる薄れていく。
三つ目が、httpOnly クッキーにセッション ID を入れる方法だ。永続性は同じく問題なく、httpOnly で XSS にも耐える。Secure 属性を付ければ、HTTPS 通信に限定することもできる。
そして殊に優れているのが、運用の柔軟性である。セッションの実体はサーバー側の Redis や DB にあるので、サーバーの判断でいつでも無効化できる。フロントで磁気を消せるカードキーと同じだ。ログイン中のデバイス一覧を出すのも、パスワード変更時にすべてのセッションを破棄するのも造作ない。
最後に、sessionStorage に JWT を置く方法もある。もっとも、これはタブを閉じた時点で消えてしまう。デバイスをまたいで永続させたいという今回の要件には、そもそも合っていない。
並べてみると次のようになる。
| 保存先 | 永続性 | XSS 耐性 | サーバー側での制御 |
|---|---|---|---|
| localStorage + JWT | ◯ | ✕ | 難しい |
| httpOnly クッキー + JWT | ◯ | ◯ | 難しい(失効管理を入れるとステートレスの利点が薄れる) |
| httpOnly クッキー + セッション ID | ◯ | ◯ | ◎ 自由に無効化できる |
| sessionStorage + JWT | ✕(タブを閉じると消える) | ✕ | 難しい |
セッション ID が選ばれる理由
現代のウェブ開発、とりわけセキュリティと込み入ったユーザー管理が求められる場面ではどうか。httpOnly クッキーとサーバー管理のセッション ID の組み合わせが、もっともバランスがよいとされている。理由は三つある。
一つ目は XSS 対策だ。httpOnly 属性のおかげで、万一スクリプトが紛れ込んでもセッション ID は盗まれない。二つ目はサーバー側の制御権である。JWT のように「一度発行したら止められない」という心配がなく、サーバーはいつでもアクセス権を取り消せる。
三つ目はデバイス管理になる。ユーザーが複数のデバイスでログインしていても、それぞれのセッションを個別に一覧・削除できる。サーバー側で状態を持っているからこその強みだ。
「スケーラビリティのために JWT を localStorage に」という誘惑は、たしかに根強い。しかし、セキュリティ事故のリスクに加え、あとで「このユーザーだけ強制ログアウトさせたい」と言われた場面を想像してほしい。途方に暮れたフロント係のごとく、発行済みのトークンが期限切れになるまで、ただ祈りながら待つことになる。
以上、セッション情報の保存先を四つ比べた。目新しさはないが、Cookie とサーバーサイドセッションの組み合わせが、今でももっとも堅実で安全な選択肢である。鍵は、フロントが止められるものに限る。結局のところ、止められるものだけが安全なのだ。