JWTを深く掘り下げる

profile image

広く使用されている認証方式であるJWT(JSON Web Token)の誕生背景から構造、動作原理まで改めて深く掘り下げ、なぜ、どのように使用すべきかを再考しました。

この記事は Jetbrains's Coding Agent Junie junie logoによって翻訳されました。誤訳があれば教えてください!

JWT(JSON Web Token)は、今日のWebアプリケーションとAPIでユーザー認証および情報交換のために広く使用されている技術です。実務でも多く使われ、面接質問でも定番として出てくるこのJWTについて知っていると思っていましたが、ある日新入社員が何かと聞いてきた時に、自信を持ってJWTとは何かと答えられない自分に気づきました。

このブログ投稿を通じて、JWTについて改めて正確に理解し、JWTがなぜ誕生したのか、その登場背景から始まり、どのように動作するのか、どのような長所と短所を持っているのかを探ってみましょう。

JWT以前の認証方式

伝統的に、Webアプリケーションの主な認証方式はセッション-クッキーベースの認証でした。この方式の動作原理は次の通りです。

session-cookie-auth.png

  1. ユーザーがログインを試みます。
  2. サーバーは情報が有効かどうかを確認し、有効であればユーザーの情報をサーバーメモリやデータベース(以下「セッションストア」)に保存します。この時、各ユーザーを識別できる固有のID(セッションID)を生成します。
  3. サーバーはこのセッションIDをクライアントに送り、クライアントはこのIDをクッキーに保存します。
  4. その後、クライアントはサーバーにリクエストを送るたびに、クッキーに保存されたセッションIDを一緒に送ります。
  5. サーバーは受け取ったセッションIDをセッションストアにある情報と比較して、ユーザーを識別し認証状態を確認します。

この方式は実装が簡単で直感的ですが、サーバーはセッションストアを作成し管理する必要があり、ユーザーが増えるほどセッションストアの負荷が増加し、それによりボトルネック現象などにつながり、サーバーが対応しきれなくなる可能性があります。

また、値をクッキーに入れて送るため、CORSポリシーによるクロスドメインの問題があり、Webブラウザ以外のモバイルアプリや他の種類のクライアントでは、クッキーを直接管理するのが面倒だったり適切でなかったりする場合があり、これはクライアントの多様性を考慮すると制約となりました。

Webの進化と新しい認証方式の必要性

2000年代半ばから、Webは単なる情報提供のWebサイトを超え、Webサービスの時代に入りました。この変化が既存の認証方式に新たな問題をもたらしました。

  • 第三者サービスの統合: ユーザーはFacebook、Googleなどのアカウントで様々なサービスにログインしたいと望んでいます。
  • API認証: 単にWebサイトにアクセスするだけでなく、APIを通じてデータをやり取りする場合にも認証が必要になりました。
  • クロスドメインの問題: クッキーは基本的に同じドメインでのみ有効なため、複数のドメインにまたがってサービスを提供したり、複数のドメイン間で認証が必要な場合に問題が発生します。

これらの問題を解決するために、今日広く使用されているOAuthが登場しました。OAuthは何度かの発展を経てOAuth 2.0に進化し、これはトークンを通じてユーザーの権限を確認する方式を採用しました。

問題は、OAuth 2.0が認証フロー(流れ)を定義したものの、実際に使用されるトークンの形式は明確に定義しなかったということです。そのため、各サービスは独自のトークン形式を使用するようになり、これは別の形の非効率性を引き起こしました。

SAML

APIベースの認証が普及する前、企業環境ではすでに**シングルサインオン(SSO)**の必要性がありました。SAMLは2002年にOASISによって開発されたXMLベースの認証および権限付与標準で、主に企業環境でSSOを実装するために使用されていました。

しかし、SAMLはXML形式であるため、ファイルサイズが大きく、パースと署名検証が複雑で、REST方式には適していませんでした。

JWT(JSON Web Token)の誕生

前述の背景をもとに、JWTは2010年代初頭、IETF(Internet Engineering Task Force)のOAuthワーキンググループによって開発されました。JWTは、2つのエンティティ間で安全にクレーム(claims)を送信するためのコンパクトで独立した方法を定義したオープンスタンダード(RFC 7519)です。

JWTの登場により標準化されたトークンが出現し、JSONベースでXML(SAML)よりデータサイズが小さく、HTMLおよびHTTP環境で効率的に転送できるようになりました。また、JWTは自身に必要な情報を含んでいるため、サーバーが別途セッション情報を保存する必要がありません。

前述の背景をもとに、JWTは2010年代初頭、IETF(Internet Engineering Task Force)のOAuthワーキンググループによって開発されました。JWTは、2つのエンティティ間で安全にクレーム(claims)を送信するためのコンパクトで独立した方法を定義したオープンスタンダード(RFC 7519)です。

JWTの登場により、以下のような問題が解決されました:

  • 標準化されたトークン: トークン形式が標準化され、サービス間の相互運用性が大幅に向上しました。
  • 効率的なデータ転送: JSONベースで作られているため、既存のXML(SAML)方式よりデータサイズがはるかに小さくなっています。そのため、HTMLおよびHTTP環境でより効率的にデータをやり取りできるようになりました。
  • サーバーのステートレス性(Stateless): JWTはトークン自体に必要なすべての情報(ユーザー識別、権限など)を含んでいます。そのため、サーバーがユーザーセッション情報を別途保存・管理する必要がありません。

JWTの構造

JWTは.を区切り文字として、ヘッダーペイロード署名の3つの部分で構成されています。

bash
xxxxx.yyyyy.zzzzz

ヘッダー

JWTのヘッダーはJWT自体に関するクレーム(情報)を含み、使用されたアルゴリズム、署名/暗号化の有無、解析方法を定義します。

json
{
  "alg": "HS256",
  "typ": "JWT",
  "cty": "JWT"
}

Note: typctyフィールドはオプションです

  • 必須ヘッダー情報
  • alg: JWTの署名/暗号化に使用されたアルゴリズム。暗号化されていないJWTの場合、値はnoneに設定されます。
  • オプションヘッダー情報
  • typ: JWTのメディアタイプ。他のJWTヘッダーオブジェクトと混合される場合に区別するために使用されます。通常はJWTに設定され、実際の使用は稀です。
  • cty: コンテンツタイプ。ペイロードが一般的なクレームとデータを含む場合は設定されません。ペイロードがネストされたJWTの場合、JWTに設定され、追加処理が必要であることを通知します。ネストされたJWTは稀なので、ctyもほとんど使用されません。

ペイロード

トークンに含める情報を含みます。これらの情報の断片はクレーム(Claim)と呼ばれます。

json
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

登録済みクレーム

JWT仕様ですでに定義されているクレームです。必須ではありませんが、推奨されるセットです。

  • iss(発行者): JWTを発行した主体を識別する固有の文字列またはURI。
  • exp(有効期限): JWTが期限切れになる時点をPOSIX形式で表した数値。
  • sub(主題): JWTが情報を含んでいる主体を識別する固有の文字列またはURI。
  • aud(対象者): JWTの意図された受信者を識別する文字列、URI、または配列。
  • nbf(有効開始時間): JWTが有効になる開始時点をPOSIX形式で表した数値。
  • iat(発行時間): JWTが発行された時点をPOSIX形式で表した数値。
  • jti(JWT ID): JWTの固有識別子文字列。

パブリッククレームとプライベートクレーム

  • パブリック: JWTを使用する人々の間で衝突を防ぐために公開的に定義されたクレームです。
  • プライベート: サーバーとクライアント間の合意の下で使用されるクレームです。ユーザーのID、名前、権限などの情報がここに含まれます。

署名

ヘッダーとペイロードをエンコードした値と、サーバーが持つ秘密鍵(Secret Key)をヘッダーに指定されたアルゴリズムで暗号化して生成します。例えば、HMAC SHA256アルゴリズムを使用する場合、次のように署名を作成します。

javascript
HMACSHA256(
  Base64UrlEncode(header) + "." + Base64UrlEncode(payload),
  secret
)

この署名はトークンの完全性を保証する役割を果たします。

組み合わせ

これで3つの値をBase64Urlでエンコードし、区切り文字(.)を通じて1つの文字列にまとめると、JWTが作成されます。

plaintext
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

JWTの認証プロセス

JWTがどのような情報を含んでいるか、その構造について理解したので、実際にどのような順序で動作するのかを見てみましょう。

Note

以下のプロセスはHS256(対称鍵)アルゴリズムを基準に説明しているため、他のアルゴリズムを使用する場合はプロセスが異なる場合があります。

jwt.png

  1. ユーザーがログインに成功すると、サーバーはユーザーの情報を含むJWTを生成してクライアントに渡します。
  2. クライアント(ブラウザ)は発行されたJWTをストレージ(ローカルストレージ、セッションストレージ、クッキーなど)に保存します。
  3. その後、サーバーにリクエストを送るたびにヘッダーにJWTを含めて送ります。
  4. サーバーは受け取ったJWTの署名を検証して、トークンの有効性と改ざんの有無を確認します。
  5. 検証が完了すると、サーバーはトークンに含まれるユーザー情報を信頼し、リクエストを処理します。

署名はどのように検証されるのか?

jwt-verify-signature.png

サーバーがクライアントからJWT(xxxxx.yyyyy.zzzzz)を受け取ると、次のような手順に従います:

  1. トークンの分離: サーバーはまず受け取ったJWTを.を基準にヘッダー(Header)、ペイロード(Payload)、署名(Signature)の3つの部分に分離します。

  2. ヘッダーとペイロードの準備: 分離されたヘッダーとペイロードを準備します。これら2つの部分は、トークンを作成する時と同様にBase64Urlでエンコードされた状態です。

  3. 署名の再生成: サーバーは自身だけが安全に保管している秘密鍵を使用して署名を再度作成します。

    javascript
    HMACSHA256(
      Base64UrlEncode(header) + "." + Base64UrlEncode(payload),
      secret
    )
  4. 署名の比較: サーバーが直接作成した署名とクライアントが送ったJWTに含まれる既存の署名を比較します。

JWTの長所と短所

長所

  • ステートレスと拡張性: サーバーがユーザーの状態を保存しないため、サーバーの負荷を減らし、水平方向に拡張しやすくなります。
  • プラットフォーム独立性: トークンベースのため、モバイル、ウェブ、デスクトップなど様々なプラットフォームとデバイスで同じように動作します。
  • セキュリティ: 署名を通じてトークンの改ざんの有無を検証できます。

短所

  • トークンの長さ: セッションIDに比べてトークンの長さが長いです。これはネットワークトラフィックに若干のオーバーヘッドを引き起こす可能性があります。
  • セキュリティに敏感な情報を保存できない: ペイロードはBase64でエンコードされるだけで、暗号化されません。誰でもデコードして内容を確認できるため、パスワードのような機密情報は含めるべきではありません。
  • トークン無効化の難しさ: JWTは一度発行されると、有効期限が切れるまで有効です。サーバーが状態を管理しないため、トークンが盗まれた場合でも、サーバーから強制的に無効化することが難しいです。

まとめ

単に「みんなが使っているから」使ってきたJWT。しかし、誰かにこの技術を明確に説明するために、その誕生背景から動作方式に至るまで、改めて振り返る時間を持ちました。この記事を読む他の誰かにとっても、JWTについて再び学ぶ機会になれば幸いです。