「デバイスをまたいでもログイン状態を維持したい」という要件が来たとする。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 とサーバーサイドセッションの組み合わせが、今でももっとも堅実で安全な選択肢である。鍵は、フロントが止められるものに限る。結局のところ、止められるものだけが安全なのだ。