Webアプリケーションのセッション管理にJWT導入を検討する際の考え方

f:id:ritou:20191126174956p:plain

おはようございます、ritou です。

qiita.com

これの初日です。

なんの話か

皆さんは今まで、こんな記事を目にしたことがありませんか?

  • Cookie vs JWT
  • 認証に JWT を利用するのってどうなの?
  • JWT をセッション管理に使うべきではない!

リンク貼るのは省略しますが、年に何度か見かける記事です。

個人的にこの話題の原点は最近 IDaaS(Identity as a Service) として注目を集めている Auth0 が Cookie vs Token とか言う比較記事を書いたことだと思っていますが、今探したところ記事は削除されたのか最近の記事にリダイレクトされてるようなのでもうよくわからん。

なのでそれはおいといて、この話題を扱う記事は

  • クライアントでのセッション管理 : HTTP Cookie vs WebStorage(LocalStorage / SessionStorage)
  • サーバーでのセッション管理 : セッションIDを保持 vs 構造体として保持

あたりの話が混在している気がします。

それぞれの細かい指摘はそれなりに正しそうなんですが、全体として「結局どうなん?」となるでしょう。なりませんか?

この記事は比較対象を細かく整理した上で、"JWTを使うために何を考えるべきか" を紹介します。

まずは、Webアプリケーションにおけるセッション管理方法について振り返ります。

Webアプリケーションにおけるセッション管理方法

Webアプリケーションにおけるセッション管理方法で整理すべきはこの2つの比較です。

それぞれを見ていきます。

Cookie vs Token in WebStorage

Webアプリケーションにおいて

  • Single Page Application によるクライアントサイドからのデータ操作が普及してきたぞ
  • OAuth 2.0 のアクセストークンのようなのを用いて自前/外部のAPIにアクセスするぞ
  • で、アクセストークンってどこに保存すればいいんだっけ?

という流れから、

  • JavaScript から読める LocalStorage に保存するとなると、XSSとかが怖い
  • JavaScript から触らせないように HTTP Cookie で HttpOnly 属性をつけたらセキュア?

というお話になります。

その結果、例えば SPA でのAPIアクセスを考えてる人は

  • SPAからのAPIアクセスに利用する期限の短いアクセストークンはリスク受容して WebStorage でもいいかな?
  • それを生成するために必要なやつは Cookie に保存してそこからアクセストークン払い出そうかな?
  • Cookie は常に送られるしサイズとかあるみたいなのででかいのは入れられないかもな... WebStorage に入れといた値と Cookie の値の組み合わせをうまくできないかな?

みたいになるかもしれません。

この部分を検討するにあたり、保存する値が JWT かどうか、それほど関係ないはずです。

まずはこの特性の違いを意識してどこにどのような値を保存するかを考える必要があるでしょう。

セッションIDを保存 vs エンコードされた構造体を保存

JWT が普及する前から、色々な言語の Web Application Framework では、Cookie を用いたセッション管理の実現方法として次のような方法が実装されてきました。

  • CookieにセッションIDを格納、セッションデータはデータストアに格納する
    • データストアには RDBMemcached などのデータストアを利用する
    • セッションIDの破棄だけではなく、データストアで保存されている値を削除することで "セッションの無効化" が可能
  • エンコードされた構造体をセッションCookieに詰め込む
    • 署名の鍵、暗号化の設定ができる
    • 署名などCookieの文字列の有効性だけを検証に利用する場合、"完全なセッションの無効化は困難"。せめて有効期限の概念が必要。

ちなみに、なんとなくCookie=セッションID、WebStorage=構造体を保存するという前提になってる節もありますが、実際はセッションIDを WebStorage 持つこともできますし、Cookieエンコードした構造体を持つこともできます。

そしてこの二つ、どちらかを選択する前提で語られることも多いですが、両者を組み合わせる方式もあるでしょう。

  • セッションIDを含む構造体をCookie/Tokenに詰め込む
    • 署名をつけられる : データストアから引く前に改ざんなどを検知可能
    • 暗号化できる : 構造体の中身を隠せる
    • セッションID以外の値も含められる : 文字列自体に有効期限などを設定できる
    • データストアがある : 無効化可能、フロントエンドに流通させたくないデータはデータストアに閉じ込めておける

両者の特徴を備えるとなるとそれなりに重い実装にはなるでしょう。 扱うデータがセンシティブであるサービスではこのようなセッション管理方式も比較対象に入れても良いでしょう。

ここまでの話で、各WAFや利用されるライブラリに依存している部分があります。

後者の実装まで考えると結果的に JWTの特徴 と大きく関連する内容にはなりますが、まずはどちらの方法を選択するべきか、あるいは組み合わせるべきかを考える必要があるでしょう。

JWTに親でも●されたかのような JWT as a Session な記事の批判は大体がここまで紹介したあたりの話かと思いますが、ここまでの話は JWT が注目される前から存在するものであり、JavaScript での操作の需要が増えて注目されてはいるものの、「枯れた実装」ならぬ「枯れた設計」でしょう。

要件に合わない場合は使う必要はないわけですが、ここからは "それでもなおJWTの導入を検討したい人" が何を考えるべきかというあたりに触れていきます。

JWTの復習

JWT は次の特徴を持つエンコードフォーマットです。

  • 様々なデータをURL Safeにエンコードできる
  • JWS(RFC7515) で署名をつけたり、JWE(RFC7516) で暗号化したりできる
  • 構造化されたデータをやりとりするための標準的な claim が定義されている(RFC7519)
  • 署名や暗号化のアルゴリズム(RFC7518)、鍵の表現(RFC7517)も豊富

これまで紹介したエンコードされた構造体」を標準化した仕様 として導入を検討すると言う意味になるでしょう。

また、(比較対象についてはおいといて)「HTTP Cookieに比べてまだまだ実績が...」みたいな主張も見かけますが、

  • 様々なプロトコルユースケースで利用されている
  • 対応ライブラリも多い
  • JWT生成、検証方法についても一般的になってる
  • 実装における脆弱性や対策も周知されている

といったあたりは「新しくて危ない仕組み」の時期はもはや過ぎているんじゃないかなと思います。 JWT をセッション管理に利用するために考えるべき点を整理します。

何の値を Payload に入れるか

個人的に属性の扱いと言うあたりでは Cookie と JWT を比較しても良いかなと思っていました。

Webブラウザは、Webサーバーが "Set-Cookie" ヘッダにより指定する属性値によってその後の挙動を決定しますが、それだけで完全に Cookie をハンドリングできるかというとそうでもありません。 例えば、Expires, Max-Ageなどで有効期限を指定してもブラウザの開発者ツールなどでユーザーが変更することもできますし、Webサーバーに送られてきたCookieは値だけなので、指定した通りに扱われていることを検証できません。

それに対し、JWT の claim に属性値を指定することで、HTTPのリクエストを受けた時点でそれが改ざんされていないことを検証できます。 RFC7519 で定義されている claim の値を利用することで、複数パーティ間のやりとりだけではなく、Webサーバーが自分自身でハンドリングする場合の検証にも利用できます。

  • iss : 発行者。セッションを発行するサーバー。
  • aud : 受信者。セッション管理の場合は自分自身など。
  • iat, nbf, exp : 発行日時、有効期限周り

Cookie や WebStorage に保存するトークンに誰が何のために発行したものか、有効期限を検証できるようにしたい場合には JWT の Payload にあるこれらの claim を意識し、Signature の検証と合わせて利用することをお勧めします。

それ以外の claim について、JWT の識別について

  • jti : JWT の識別子として「セッションIDを含む構造体をCookieに詰め込む」場合のセッションIDを指定

が使えます。データストアと組み合わせる際にも使えるでしょう。

ユーザー識別子としては

  • sub : ログインセッションの対象となるユーザーIDを指定

が使えます。

識別子の管理ポリシーに従ってPPIDを入れたりJWEを用いて暗号化しても良いでしょう。

JWT の検証パターンと署名アルゴリズム

Cookie や Bearer Token としてなどの HTTPリクエストにJWTの値が含まれる場合、段階的な検証になるかと思います。

  1. 用途(cty あたりで識別)や署名の検証
  2. jti を用いたセッション検証 : セッションDBの参照(※データストアと組み合わせるなら)

署名アルゴリズムについて、monolithなサービスで発行/検証が同一モジュールで行われるような場合は共通鍵暗号方式でも良いと思います。 サービスの規模が比較的大きくなったりしてこれらの検証処理が発行処理と別のところで行われる(署名検証などはProxyやAPI Gatewayみたいな前の方でやり、必要に応じてアプリケーションでデータストアを引くなど)場合、公開鍵暗号方式に変えて検証用の公開鍵を検証箇所に配布するような設計も必要になるかもしれません。

ちょっと前にJWT の署名検証あたりのノウハウについては Qiita に記事を書いたことがあります。

qiita.com

alg がどうこうな話とかは運用で十分に抑えられるでしょう。

終わりに

今回の記事のネタである Cookie vs JWT みたいな比較記事は年に何度かでてくるものの、"JWTだからこそ●●" という部分にはあまり触れらていないものが多いと感じていました。

2011年頃の仕様策定中から JWT をちょっと離れたところから見守ってきた(?)私としては

eyJちゃん、可愛らしイネ(^o^)😃✋ホント可愛すぎだよ〜😄マッタクモウ😃😍🎵💗

(by おじさん文章ジェネレーター)

という感じではありつつも、あくまで "構造化されたデータを安全に送受信するための標準化された仕様" 以上でも以下でもないですし、特徴を活かせそうなところに使っていけば良いと思います。

また、JWT と関連する仕様として、バイナリデータが利用できる環境においては CBOR Web Token (CWT) あたりの活用も少しずつ広まるのかなと感じています。 乱暴にいうと CWT ってのはエンコード方法としてBase64JSONを使っているところをCBORというエンコード方式を利用するものであり、Payload に送受信したいデータを入れるところや署名検証あたりの設計思想は JWT とほとんど一緒です。 今後はJWTを適用しにくいユースケースにおいて構造化されたデータをやりとりしたい時に CWT が検討されていくでしょう。

今回の記事により、有識者に対する「JWTってどうなんです?」という漠然とした質問、JWTの導入検討の悩みを軽減できることをお祈りしています(突然のお祈り)。

明日(2日目)の 「認証認可技術 Advent Calendar 2019」は...また私です。

ではまた。