RFC7519で定義されたJWT(JSON Web Token):作成から署名までの概念フロー

知識

個人的に認証・認可の仕組みを学びたいと思っていたが、中々手出しできずにいました。そんな中今回はやっと自分の重い腰を上げてこの仕組みを一から学んでみようと思います。まずは認証機能でよく利用されているJWTについてまとめてみました。

JWT を使用した認証プログラムを作成してみましたので以下の記事も参考に。



JWTとは?

署名によって改ざんを防止した情報を安全にやり取りするためのデータ形式。主にWebサービスの認証や認可に利用され、ユーザー情報などをJSON形式でコンパクトに保持できるのが特徴。JWTはRFC7519で定義されています。
https://datatracker.ietf.org/doc/html/rfc7519



JWTの構成

JWTは「Header」、「Payload」、「Signature」 をそれぞれドット(.)で区切ったBase64URLエンコード形式にした形で構成されており、最終的には以下のよう形式となります。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

上記はエンコードされた形式ですが、エンコードされる前の各構成部分を確認しておきます。

Header(ヘッダ)
アルゴリズムやトークン種別を指定。ここで指定されたアルゴリズムがSignature(署名)作成のために使用されます。

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


Payload(ペイロード)
ユーザIDや有効期限などのクレームを指定。ペイロードは、JWTの「中身」に当たる部分で、やり取りしたいデータ本体が含まれます。クレームとは、英語で「主張」という意味で 「このユーザーのIDは123である」「このトークンの有効期限は〇時までである」といったそのトークンが主張(伝達)したい情報の一つひとつを指します。

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

またペイロード内にはパスワードなどの機密情報は入れてはいけません。ペイロード自体はBase64URLでエンコードされてますが、これは誰でもデコードしてエンコードされる前のパスワードを閲覧できてしまうからです。



Signature(署名)
署名は、Header / Payload / 鍵 / Headerで指定されたアルゴリズムを使用して作成されます。

Signature = HMACSHA256(base64UrlEncode(Header) + “.” + base64UrlEncode(Payload), secret_key)​

署名の目的はデータの改ざん検知と送信元(サーバー側)の真正性検証のために使用されます。
なぜ署名により「改ざん検知」と「本人確認」ができるのか?

  • データの改ざん検知: もし誰かが「Payload」の中身を1文字でも書き換えると、同じ計算をしても計算結果(署名)が全く別の値になる。受け取った側が自分で計算した結果と、送られてきた署名が一致しなければ、「これは偽造されたものだ!」とすぐに分かる。
  • 送信元(JWT発行者)の検証: 正しい署名を作るには、発行者しか知らない「秘密鍵」が必要。正しい署名がついているということは、「秘密鍵を持っている本人が作った証拠」になる。



JWT作成の流れ

JWTの構成を理解したのでここではそれらを使用してサーバー側でJWTがどのように作成されるかを確認していきます。

①.Headerの作成
アルゴリズム(alg)とタイプ(typ)を定義し、HeaderのBASE64URLエンコードを作成→Encoded_Header


②.Payloadの作成
ユーザーID(sub)や有効期限(exp)などを定義しPayloadのBASE64URLエンコードを作成→Encoded_Payload


③.署名対象の結合
Encoded_Header + “.” + Encoded_Payload という文字列を作成


④.署名の生成
③の文字列と指定されたアルゴリズムと鍵(秘密鍵)を用いて署名を作成します。

署名プロセスは大きく分けて「共通鍵方式(HS256)」と「公開鍵方式(RS256)」の2パターンあります。

発行者と検証者が「同じ秘密鍵」を持つ方式です。

ハッシュ化の仕組み:HMAC(Hash-based Message Authentication Code)という仕組みを使います。

具体的な流れ
encoded_header + “.” + encoded_payload というデータ(メッセージ)を用意します。このデータと秘密鍵を組み合わせて、SHA-256 などのハッシュ関数に通します。出力された固定長のバイナリデータが「署名(Signature)」となります。

特徴
鍵が漏洩すると、誰でも偽造が可能になります。小規模なシステムや、内部通信での利用に向いています。

発行者が「秘密鍵」、受信者(検証者)が「公開鍵」を使うよりセキュアな方式です。

ハッシュ化と署名のステップ

  1. データの連結:Encoded_header と Encoded_payload をピリオド( . )で繋ぎます。
  2. ハッシュ化:その文字列 SHA-256 アルゴリズム(ハッシュ関数)に通し、固定長のハッシュ値を作成します。
  3. 暗号化(署名):そのハッシュ値を発行者だけが持つ秘密鍵を使って暗号化します。これが署名(Signature)となります。
  4. 検証の仕組み(受信者側):受信者は送られてきた「署名」部分と「ヘッダー・ペイロード」を切り離します。
    • 復号:発行者の公開鍵を使って、送られてきた署名を復号し、発行者側で計算された「ハッシュ値A」を取り出します。
    • 再計算:手元にあるヘッダーとペイロードから、受信者側でも SHA-256 を使って「ハッシュ値B」を計算します。
    • 比較:A と B それぞれのハッシュ値が完全に一致すれば検証が合格となります。

「任意の長さのデータを、固定長のバイナリデータ(数値の羅列)に変換する計算」を指します。不可逆性であり、ハッシュ化されたデータは元に戻せません。

入力:どんなデータでもOK(万能性)
文字列だけでなく、画像、音声、実行ファイルなど、どんなデータでも入力できます。

出力:常に固定長(一貫性)
常に決まった長さ(例:SHA-256なら必ず256ビット)のバイナリデータや文字列になります。署名対象のデータがどれだけ大きくても、署名計算(秘密鍵での演算)は固定サイズのハッシュ値に対して行うだけで済むため、非常に高速に処理できます。

なぜ混乱しやすいのか:
コンピュータ上では、最終的にすべてが 0 と 1 のバイナリで処理されますが、人間には読めません。そのため、ハッシュ値を確認する際は「16進数の文字列」(例:5e8848…)として表示されることが一般的です。 「文字列をバイナリにする」というよりは、「データを複雑に計算して、そのデータ固有の指紋(バイナリ)を生成する」と捉えるのが正確です。

もしハッシュ化せずに、巨大なペイロード全体をそのまま秘密鍵で署名(暗号化)しようとすると、以下の問題が発生します。

処理速度:
公開鍵暗号(RSA等)による計算は非常に重いため、ペイロードの長いデータに直接適用すると時間がかかりすぎます。

データのサイズ:
ハッシュ化することで、どんなに長いペイロードでも数十バイトの固定長データに凝縮でき、効率的に署名を行えます。
※SHA-256の場合は、256ビット(32バイト)の短いデータに凝縮できます。

⑤.署名のエンコード
④で得られたハッシュ値(バイナリデータ)をBase64URLエンコード

⑥.最終結合
①②⑤をドット( . )で繋ぐことでJWTが完成します。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30



クライアント側でのJWT保持

サーバー側で作成されたJWTはHTTPレスポンスボディ(またはレスポンスヘッダ)を通じでクライアントへ送られ、その後の通信で利用されます。

  • レスポンスの方法
    • レスポンスボディにセットする:パターン1
      JSONレスポンスの中に直接含める方法。フロントエンド(JavaScript)で扱いやすいため多くのWeb APIで採用されています。
    • レスポンスヘッダ(Set-Cookie)にセットする:パターン2
      サーバー側で Set-Cookie ヘッダーを使い、ブラウザのCookieに保存させる方法です。次回の通信から自動でサーバー側に送られます。
  • クライアント側でのJWTの保持方法
    • LocalStorageに保存(パターン1のとき)
    • Cookieに保存(パターン2のとき)

それぞれの特徴は以下の表のようになります。

比較項目LocalStorageCookie (HttpOnly)
保存先ブラウザのローカルストレージブラウザのCookie管理領域
JavaScriptからのアクセス可能(localStorage.getItem()不可(サーバーのみが操作可能)
CSRF攻撃影響を受けない対策が必要
XSS攻撃脆弱(トークンを盗まれる可能性あり)強い(JavaScriptから盗めない)
物理的な保存場所クライアント(ブラウザ)クライアント(ブラウザ)
保存を実行する人クライアント側のJavaScriptブラウザ(サーバーの命令[Set-Cookieヘッダー])
主導権クライアントが自由に読み書きするサーバーが「保存せよ」と命じる

Set-Cookieの形式

Set-Cookie: Name=Value; Path=/; Domain=.example.com; Secure; HttpOnly; SameSite=Strict

  • 各属性は;(セミコロン)で区切られる
  • Set-Cookie はレスポンスヘッダーに付与して、クライアントに送信される。
属性説明設定例
Name=ValueCookie名と値(必須)jwt-Token=abc123
PathCookie送信対象のパスPath=/(ドメイン以下の全ルートで有効→認証が必要な全エンドポイントでCookieが自動的にリクエストに含まれる), Path=/api
DomainCookie送信対象のドメインDomain=.example.com(サブドメイン共有)
Expires有効期限(日付)Expires=Wed, 07 Jan 2026 12:00:00 GMT
Max-Age有効期限(秒数)Max-Age=3600(1時間)
SecureHTTPS通信時のみ送信Secure
HttpOnlyJavaScriptからアクセス不可(XSS対策)HttpOnly
SameSiteCSRF対策(Strict/Lax/None)SameSite=Strict

詳しくは HTTP Cookie の使用



クライアント側からサーバー側へのJWT送信

クライアント側からサーバー側へのJWT送信については以下の二つの方式がとられます。

  • Authorizationヘッダ方式
    • トークン管理も送信も全部フロント実装。柔軟だがXSSに弱くなりがち。
Authorization: Bearer <JWTの中身>

※ Bearer(ベアラ)とは「持参人」という意味で「このトークンを持っている人を許可してください」という意味合いになります。

  • Cookie方式(Cookie)
    • トークンの保存・送信はほぼブラウザ任せで、クライアントは「Cookieが自動で付く前提」でAPIを叩くのみとなります。
    • ただし別オリジン時の credentials 設定やCSRF対策など、「周辺の設定」はクライアントでも意識する必要があります。



JWT検証の流れ

クライアントからのJWTを受け取った後にサーバー側でJWTの検証が行われます。今回は共通鍵暗号方式の場合で検証を行なっていきます。

  1. 形式のチェック
    • JWTの基本形式(encoded_header.encoded_payload.encodeed_signature の3段構成)になっているかを確認します。
      • ドット(.)で分割し、3つの要素があるか。
      • それぞれがBase64URLで正しくデコードできるか。
  2. 署名の検証(最重要)
    • ここがセキュリティの肝です。サーバーは受け取ったJWTから「署名」を自ら再計算して比較します。
      • 受け取ったJWTの HeaderPayload (エンコードされたまま)を取り出す。
      • 取り出した HeaderPayload をドット( . )で繋いだ形式を作成する。
      • Headeralg を確認します。
      • サーバーが保持している秘密鍵(HS256:共通鍵暗号方式の場合)を用意する。
      • 作成時と同じアルゴリズムを使い、手元の鍵で再度署名を計算する。
        計算式: HMACSHA256(受け取ったHeader + "." + 受け取ったPayload, サーバーの秘密鍵)
      • 署名の照合(比較)
        1. 文字列で比較する場合:再計算した署名を Base64URL形式に変換し、JWTの第3セクションの文字列と突き合わせる。
        2. バイナリで比較する場合:JWTの第3セクションをデコードしてバイナリに戻し、再計算した署名の結果と 「1ビット(1バイト)でも違うか」を確認する。
  3. 有効期限(exp)の確認
    • 署名が正しくても期限切れのJWTは無効です。ペイロードをデコードし、exp クレームを確認します。現在時刻が exp を過ぎていれば、有効期限切れとして拒否します。
  4. その他のクレーム検証
    • 必要に応じて、以下の内容もチェックします。
      • iss (Issuer): 発行者が自分のシステムであるか。
      • aud (Audience): そのJWTが自分のサービス宛に発行されたものか。

ここまでの検証を行い署名が正しいと判断されれば、サーバーはペイロードの情報を「信頼できるデータ」として扱い具体的な認可処理や業務ロジックへと処理が進みます。

合わせて以下の記事も参考に。

コメント

タイトルとURLをコピーしました