Carpe Diem

備忘録

JWTを認証用トークンに使う時に調べたこと

概要

JWTを認証用トークンに使う時に調べたことをまとめます。

JWTとは

以下のフォーマットです。

{base64エンコードしたheader}.{base64エンコードしたclaims}.{署名}

以下の特徴があります。

  • 発行者が鍵を使ってJSONを署名(or HMAC)し、トークンとして扱う。
  • 暗号化ではないので、JSON の中身は誰でも見られる。
  • 発行者は鍵を使ってメッセージの検証ができるので、改竄を検知できる。

以上の点からトークンとして向いているため、認証トークンとして用いられるようになってきました。

Cookieとの認証フロー比較

f:id:quoll00:20160607031916p:plain

ref: Cookies vs Tokens. Getting auth right with Angular.JS

認証フロー自体はほぼ同じです。CookieCookieヘッダを使い、TokenはAuthorizationヘッダを使います。

Tokenのメリット

  • Cookieに依存しないため、CSRF脆弱性がなくなる
  • ネイティブプラットフォームで扱いやすい
  • 特定の暗号スキームに依存しない(=JWT以外も扱える)
  • ステートレス。サーバ側にセッションストアも要らない。

JWTに入れるもの

最低限入れるものは以下です。

ヘッダ

Header名 説明
alg 署名アルゴリズムRSA-SHA256など
typ トークンタイプ。普通はJWT

※注意として、algにnoneHMAC-SHA*を指定することで署名検証を逃れることのできる脆弱性があるので、実装時はアルゴリズムのチェックは必ず行ってください。

JWS 実装時に作りがちな脆弱性パターン - OAuth.jp

クレーム

Claim名 説明
sub ユーザ識別子。userID(もしくはそれを暗号化したもの)といった、重要ではないがユーザを識別できるためのID
exp トークンの有効期限

その他あった方が後々便利かも、と言った要素。

Claim名 説明
iss トークン発行者。サービス名を入れる
iat トークンの発行時間
scope アクセスするリソースサーバで認可されているリソース

前述した通り、base64で変換しているだけなので、中身は誰でも見れてしまいます。なので重要な情報は絶対にいれないでください。

Tokenの送信に使うヘッダ

Authorizationヘッダに対し、Bearerスキームを設定するのが一般的のよう。

例)

Authorization: Bearer <token>

Tokenの発行フロー

f:id:quoll00:20200206021657p:plain

ref: Refresh Token: どのような場合に使用し、どのように JWT と相互作用するか

↑の図で言うAccess TokenがJWTにあたります。

理想は図のように

サーバ 役割 JWTとの関係性
Authサーバ 認証&認可を行う JWTを発行(署名)
リソースサーバ 認可されていればリソース(機能)を提供する
(=通常のAPIサーバ)
JWTの署名検証を行う

と分けてシステムを構築するのがベストです。

が、モノリスなシステム構成であればAuthサーバとリソースサーバを同一にするケースもよくあります。

Tokenの保存先はどこにすべきか

Tokenの保存先は主にCookie or localStorageがある。

Cookieのケース

メリット

デメリット

  • Cookieヘッダでサーバへ送る場合はCSRF脆弱性が残る
  • 単なる保存先として利用し、Authorizationヘッダで送る場合はCSRFを防げるが、上記のhttpOnly属性が使えない

localStorageのケース

メリット

デメリット

※localStorageはcookieと異なりプロトコル(http, https)まで一致するかを見て同一生成元ポリシーを適用するので、デフォルトでsecure属性があるものと言えます

結論

つまりどちらも一長一短であるため、以下の点に注意してどちらを採用するかはXSSCSRFの各脆弱性がサービスにどれくらいの影響を与えるかを考慮すべきです。

  1. tokenをcookieにいれ、secure・httpOnly属性をつける。サーバへの渡し方はCookieヘッダ。ただしCSRF脆弱性は残る。重要な処理に「パスワード再入力」といった対策を挟む。
  2. tokenをcookie or localStorageにいれ、AuthorizationヘッダにBearerスキームでtokenを渡す。ただしXSS脆弱性があるとセッションハイジャックされるので、フレームワークを利用してXSS脆弱性を排除する。

CSRFXSSの違い

CSRF

Cross-site Request Forgery(リクエス強要)という名前の通り、攻撃者が正規ユーザを使って意図しないリクエスト(ただし正規ユーザからの正しいリクエスなのでサーバは受け取ってしまう)を実行し、サーバ側のデータを書き換える手法。

CSRF脆弱性は見落としやすく、対応が大変ではありますが、書き込み系の重要な処理にのみ対応すれば大丈夫です。よくあるのはパスワードの再入力や、トークンを用いて対策するやり方です。

XSS

クライアント側にスクリプトを注入し、ページの書き換え(クレカ入力フォームを勝手に入れこんだり)やスクリプトを実行して(document.cookieでセッションIDをハイジャックしたり)ユーザに被害を与える手法。

入力フォームやURLパラメータと言った要素に脆弱性が生まれやすい。
例)

  • 入力フォームの確認画面で入力値をそのまま表示
  • 検索欄の入力値を検索結果画面にそのまま表示

入力要素をきちんとエスケープしたり、View用フレームワークを用いれば基本的には防げます。

同一生成元ポリシーとXSSの関係

同一生成元ポリシーは罠ページからオリジナルページへのJSの実行を拒否する仕組みです。なので罠ページにiframeなどでオリジナルページを入れてもcookieやlocalStorageにはアクセスできません。

しかしXSSは罠ページからオリジナルページに対しスクリプトを注入し、オリジナルページ側でJSを実行させることで同一生成元ポリシーを回避する手法です。

セッションハイジャックされたらどうなる?

トークンには基本的に期限expをつけますが、その期間までは有効に扱えます。
ログイン期間を増やすために、AccessトークンとRefreshトークンを分ける実装がありますが、Refreshトークンの方は盗まれた時の影響が大きいため、revokeする機能も実装する必要があります。

HMACとデジタル署名の違い

JWTはRS系やHS系といった、複数のアルゴリズムが用意されています。

HMAC

できること

  • メッセージが改竄されてるかチェック
  • 期待した通信相手からのものであるかチェック
    • 通信相手同士で鍵を共有すれば、メッセージに対して同じハッシュ化をすることができるため。

できないこと

  • 三者に対する証明
    • 三者は鍵を持っていないから
  • 否認防止
    • 複数の人が鍵を持っている場合、誰でもハッシュ化できるため、誰がこのメッセージを作ったのかが分からない。鍵を持っている人は「私はこのメッセージを作っていない」と否定できない

デジタル署名

できること

  • 三者に対する証明
    • 公開鍵を渡せばいいだけなので。
  • 否認防止
    • 秘密鍵を持つのは本人のみで、HMACのように鍵を共有する必要がないため。

JWTではどちらを使うべきか

トークン発行フローで述べたように認証&認可サーバとリソースサーバが別であれば、HMACでは機能が足りないためデジタル署名にすべきです。
JWTを発行する認証&認可サーバでは秘密鍵を保持し、各リソースサーバではペアとなる公開鍵を利用して署名検証を行います。

認証&認可サーバとリソースサーバが同一であれば、HMACの方で問題ありません。
鍵長も短く済み、処理も速いためです。

ソース