FlowingDev

JWT解説:自分自身のパスポートを持つトークン

JSON Web Token(JWT)って何?パーティ間で情報をJSONオブジェクトとして安全にやりとりするための、コンパクトで自己完結型の標準規格を解き明かします。

ツールを試す: JWT ビューア

一言でいうと

JSON Web Token(JWT)とは、二者間でやり取りされるクレーム(主張)を表現するための、コンパクトでURLセーフな方法です。通常、検証・信頼できる方法で認証や認可に使われます。

それが解決する問題

昔々、まだウェブが若かった頃――たとえば2000年代初頭とかね――ウェブサイトにログインすると、サーバーはあなたのための「セッション」を作成していました。それはサーバー上にある、「ユーザー123はログイン済みで、ショッピングカートにゴム製のニワトリを入れた」みたいなことが書かれた小さなファイルのようなものでした。サーバーはブラウザにセッションIDの入った小さなクッキーを渡します。これはクロークの預かり札みたいなもんです。その後のリクエストのたびに、ブラウザはその預かり札を提示し、サーバーはそれを参照してあなたのファイルを見つけ、あなたが誰であるかを思い出していました。

これは単一のモノリシックなサーバーならうまく機能しました。しかし、その後Webは爆発的に成長します。マイクロサービス、シングルページアプリケーション(SPA)、モバイルアプリがすべて同じバックエンドと通信するようになりました。さて、ログインリクエストはサーバーAに行くかもしれませんが、次のプロフィール取得リクエストはサーバーBに行くかもしれません。サーバーBはサーバーA上のセッションファイルをどうやって知るのでしょうか?

ユーザーが常に同じサーバーと通信するように強制する(「スティッキーセッション」)こともできますが、それはボトルネックになります。すべてのサーバーが共有する中央集権的なセッションデータベース(Redisなど)を作成することもできますが、それは管理すべきインフラがまた一つ増え、障害点も増えることになります。

根本的な問題はステートフルであること。サーバーがあなたのことを覚えておかなければならないんです。

JWT(「ジョット」と読みます)はこの考えを根底から覆しました。もしユーザーがパスポートみたいに、自分自身の身分証明を持ち運べたらどうでしょう? トークン自体が、サーバーが必要とするすべての情報、つまりユーザーが誰で、何が許可されていて、いつアクセスが期限切れになるか、といった情報を含んでいるのです。サーバーはリクエストの合間に何も覚えておく必要がありません。これがステートレスな認証であり、スケーラブルな分散システムを構築するための鍵となります。サーバーはただ、そのパスポート(JWT)が有効で、偽造されていないかを確認するだけでいいんです。

舞台裏の仕組み

JWTは、わけのわからない意味不明なデータのかたまりじゃありません。ドット(.)で区切られた3つの部分からなる、非常に明確で構造化された文字列なんです。

xxxxx.yyyyy.zzzzz

各部分を分解してみましょう。

ヘッダー(「Type」タグ)

最初の部分はヘッダーです。これはトークン自体に関するメタデータを含む単純なJSONオブジェクトで、主に使用される署名アルゴリズムとトークンのタイプが含まれています。

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg: 署名アルゴリズム。HS256は、このトークンがHMAC-SHA256(共通鍵アルゴリズム)で署名されていることを意味します(これについては後ほど詳しく)。他の一般的なオプションには、RSAの公開鍵/秘密鍵ペアを使用するRS256などがあります。
  • typ: トークンのタイプ。JWTの場合、これは単に "JWT" です。

このJSONは、トークンの最初の部分を生成するためにBase64Urlエンコードされます。Base64はエンコード方式であって、暗号化ではありません。バイナリデータをWeb上で安全に転送できるテキスト文字列に変換するだけです。ハガキの裏に「これはハガキです」と書くようなものだと考えてください。途中で誰かに盗み見られたら、中身は丸見えです。

ペイロード(「Claims」部門)

2番目の部分はペイロードです。ここがキモ。ユーザー(「サブジェクト」)に関する「クレーム(claims)」という表明や、その他の有用なデータを含む、もう一つのJSONオブジェクトです。

{
  "sub": "10987-23456-98765",
  "name": "Grace Hopper",
  "admin": true,
  "iat": 1516239022,
  "exp": 1516242622
}

クレームには3つの種類があります:

  • 登録済みクレーム: 相互運用性を提供するために事前定義された、推奨クレームのセットです。必須ではありませんが、非常に便利です。
クレーム 名前 説明
iss 発行者 (Issuer) トークンを発行した者 (例: https://api.mycoolsite.com)。
sub サブジェクト (Subject) トークンが対象とするユーザーやエンティティ (例: ユーザーID)。
aud オーディエンス (Audience) トークンの意図された受信者 (例: https://api.mycoolsite.com)。
exp 有効期限 (Expiration Time) トークンの有効期限。Unixタイムスタンプの数値 (エポックからの秒数)。
iat 発行日時 (Issued At) トークンが発行された日時。これもUnixタイムスタンプ。
  • 公開クレーム: 自分で作成するカスタムクレームですが、命名の衝突を避けるために、IANA JSON Web Token Claimsレジストリで定義されるか、衝突を回避できる名前空間を含むURIであるべきです。
  • プライベートクレーム: 最も一般的なカスタムクレームで、使用に合意したパーティ間で情報を共有するために作成されます(例のadmin: trueのように)。ここにアプリケーション固有のデータを入れます。

ヘッダーと同様に、ペイロード全体のJSONも、JWTの2番目の部分を形成するためにBase64Urlエンコードされます。繰り返しますが、これは暗号化されていません。ペイロードにはパスワードのような機密情報を絶対に入れないでください。

署名(改ざん防止シール)

ここがセキュリティを提供する部分です。署名は、JWTの送信者が本人であることを確認し、メッセージが途中で変更されていないことを保証するために使用されます。

エンコードされたヘッダー、エンコードされたペイロード、秘密のキー(シークレット)を、ヘッダーで指定されたアルゴリズムで処理することによって作成されます。今回のHS256の例では、プロセスは次のようになります:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

ここがミソなんですが、サーバーだけが知っているsecret(秘密情報)が使われるんです。サーバーがJWTを受け取ると、受け取ったヘッダーとペイロードを使って、まったく同じ計算を再実行します。生成した署名がトークン上の署名と一致すれば、サーバーは2つのことを知ることができます:

  1. 真正性: トークンは秘密のキー(つまり、サーバー自身)を知っている誰かによって作成されたこと。
  2. 完全性: ヘッダーとペイロードが改ざんされていないこと。もし攻撃者がペイロードの"admin": falseを"admin": true"に変更したら、署名は一致しなくなります。

この署名こそが、我々のパスポートに貼られた、改ざん防止のホログラムシールなのです。

実世界でのストーリー

マイクロサービスの迷宮

急成長中のEコマース企業「ScaleFast」社は、巨大なモノリシックバックエンドを、ユーザー用、注文用、在庫用などのマイクロサービスの集合体に分割することにしました。古いシステムではサーバーサイドセッションを使用していましたが、新しい世界では、OrderServiceはリクエストが本当にログインしたユーザーからのものであることを、リクエストのたびにUserServiceを呼び出すことなく、どうやって知ることができるでしょうか?それでは遅すぎて、分離した意味がありません。

解決策はJWTでした。ユーザーがログインすると、新しいAuthServiceがuserIdとrolesを含むJWTを発行します。ユーザーのブラウザは、他のマイクロサービスへのすべてのリクエストのAuthorizationヘッダーにこのJWTを含めます。OrderServiceとInventoryServiceはAuthServiceと通信する必要はありません。共有された秘密のキーを知っていれば十分です。それぞれが独立してJWTの署名を検証し、中のuserIdを信頼してリクエストを処理できます。

教訓: JWTはマイクロサービス認証の共通言語(リンガ・フランカ)であり、サービスがステートレスで独立して検証可能になることを可能にします。

シングルページアプリ物語

ある開発者のAlexさんは、洗練されたReactダッシュボードを構築していました。フロントエンドは静的ホストから提供されるシングルページアプリケーション(SPA)で、別のバックエンドAPIと通信していました。Alexさんは、フロントエンドとバックエンドが異なるドメインにあるため、クロスオリジンリソース共有(CORS)の問題の悪夢に直面し、昔ながらのクッキーベースの認証と格闘していました。

チームはJWTに切り替えました。今では、ユーザーがユーザー名とパスワードでログインすると、APIはJWTを返します。AlexさんのReactアプリはこのトークンをメモリに保存し、すべてのAPI呼び出しに添付します:Authorization: Bearer <the-jwt>。APIバックエンドはステートレスです。各受信リクエストでベアラートークンをチェックするだけです。もうCORSのクッキー問題で頭を悩ませることはありません。

教訓: JWTは、現代的なフロントエンドアプリケーションとバックエンドAPIをきれいに分離する、ポータブルなクレデンシャルを提供します。

よくある間違いと落とし穴

  • ペイロードに機密情報を入れてしまう。 やめて! ペイロードはBase64Urlエンコードされているだけで、簡単に元に戻せます。暗号化されているわけではないんです。トークンを手に入れた人なら誰でもペイロードを読むことができます。封をされた手紙ではなく、ハガキのように扱いましょう。
  • 署名の検証を忘れる。 国境警備員が確認しないなら、パスポートのセキュリティ機能に何の意味があるでしょう? 署名を検証せずにペイロードをデコードしてその内容を信用するのは、致命的なセキュリティ脆弱性です。攻撃者は好きなペイロードを偽造できてしまいます。
  • algヘッダーを盲信する。 過去の有名な脆弱性として、攻撃者がトークンを作成し、ヘッダーを{"alg": "none"}に変更するというものがありました。一部の設定が不適切なライブラリは、"none"を見て、何もしないことで署名を「検証」し、偽造されたトークンを有効なものとして受け入れてしまいました。常にサーバー側で、期待する特定のアルゴリズム(例:HS256)を強制するようにしてください。
  • 共通鍵(シークレット)を漏洩させてしまう。 HS256のようなHMACアルゴリズムでは、秘密のキーは王国の鍵です。それが漏洩すると、誰でも、どんなユーザーでも、どんな権限でもトークンを偽造できてしまいます。パスワードのように守りましょう。
  • 有効期限(exp)クレームを設定しない。 永遠に生き続けるトークンは巨大な負債です。もしそれが侵害された場合、攻撃者は無期限にそれを使用できます。常に合理的に短い有効期限を設定し、より長いセッションのためにはリフレッシュトークン機構を使用しましょう。

なぜ注目すべきか

分散環境で認証や認可を扱うときは、いつでも「JWT」を思い浮かべるべきです。

  • シングルページアプリ(SPA)やモバイルクライアント向けのAPIを構築している。
  • サービスがお互いからのリクエストを信頼する必要があるマイクロサービスアーキテクチャを設計している。
  • 共有セッションストアなしで水平にスケールできるステートレス認証が必要。
  • パスワードリセットリンクやメール検証など、自己完結型で有効期限付きのトークンが最適な、一度きりの認可フローを実装している。

これは、クレームを安全に表現するための現代的な標準であり、その長所と短所を理解することは、現代の開発者にとって、避けては通れない必須知識です。

もっと深く

  • RFC 7519: JSON Web Token (JWT) の公式仕様。すべての真実がここに。
  • jwt.io: ライブデバッガーや、ほぼすべての言語用のライブラリリストを備えた素晴らしいリソース。
  • OWASP JWT Cheat Sheet: JWTを使用する際のセキュリティベストプラクティスと落とし穴に関する必須ガイド(アドバイスは言語に依存しません)。
  • MDN Web Docs: Authorization header: JWTを送信するために一般的に使用されるBearer認証スキームについて学びます。
  • Wikipedia: JSON Web Token: コンセプトとその歴史に関する、優れた高レベルの概要。

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: JWT ビューア