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

知識

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

JWTとは?

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

JWTの構成

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

各構成について(BASE64エンコード前)

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

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

Payload(ペイロード)
ユーザIDや有効期限などのクレームを指定。ペイロードは、JWTの「中身」に当たる部分で、やり取りしたいデータ本体が含まれます。

クレームとは、英語で「主張」という意味。 「このユーザーのIDは123である」「このトークンの有効期限は〇時までである」といった、そのトークンが主張(伝達)したい情報の一つひとつを指す。

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

またクレームには3つの種類があることも抑えておきます。

  • Registerd Claims
  • Public Claims
  • Private Claims

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

Signature(署名)
Header、Payload、(秘密 or 公開)鍵、Headerで指定されたアルゴリズムから作成される。 これは主にデータの改ざん検知と送信元(発行者:サーバー側)の真正性検証のために使用される。

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

なぜ署名により「改ざん検知」と「本人確認」ができるのか?

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

JWT作成の流れ

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

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

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

3.署名対象の結合
encoded_Header + “.” + encoded_Payload という文字列を作成

4.署名の生成
3の文字列を、指定されたアルゴリズムと鍵(秘密鍵)を用いてハッシュ化・署名

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

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

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

具体的な流れ:

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

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

発行者が「秘密鍵」、検証者が「公開鍵」を使う、よりセキュアな方式

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

  1. データのハッシュ化: encoded_header + “.” + encoded_payload をハッシュ関数(SHA-256)にかけ、データの「指紋」であるハッシュ値を作成
  2. 署名(作成): 送信データとなるハッシュ化されたハッシュ値を算出し、発行者(サーバー側)の秘密鍵を用いて計算処理を行います。この結果得られるデータが「署名」
  3. 検証の仕組み:検証者は、ペアとなる公開鍵を使って署名を検証(復号)します。検証して得られたハッシュ値と、手元に届いたデータから自ら計算したハッシュ値を比較します。 これらが一致すれば、以下の2点が証明されます。
    • 非改ざん性: データが途中で書き換えられていないこと。
    • 真正性: 対になる秘密鍵を持つ「正しい発行者」が作成したこと。


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

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

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

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

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

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

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

5.署名のエンコード
4で得られたバイナリデータをBASE64URLエンコード

6.最終結合
1.2.5 をドットで繋いでJWTが完成します。

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

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

レスポンスの方法

パターン1:レスポンスボディにセットする
JSONレスポンスの中に直接含める方法。フロントエンド(JavaScript)で扱いやすいため、多くのWeb APIで採用されています。

パターン2:Set-Cookieヘッダにセットする
サーバー側で Set-Cookie ヘッダーを使い、ブラウザのCookieに保存させる方法です。次回の通信から自動でサーバー側に送られます。

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

  • LocalStorageに保存(パターン1のとき)
  • Cookieに保存(パターン2のとき)

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

比較項目LocalStorageCookie (HttpOnly)
保存先ブラウザのローカルストレージブラウザのCookie管理領域
JavaScriptからのアクセス可能(localStorage.getItem()不可(サーバーのみが操作可能)
CSRF攻撃影響を受けない対策が必要
XSS攻撃脆弱(トークンを盗まれる可能性あり)強い(JavaScriptから盗めない)
物理的な保存場所クライアント(ブラウザ)クライアント(ブラウザ)
保存を実行する人クライアント側のJavaScriptブラウザ(サーバーの命令[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

Set-Cookieの形式

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

クライアント側からサーバー側への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から「署名」を自ら再計算して比較します。

  1. 受け取ったJWTの HeaderPayload (エンコードされたままの文字列)を取り出す。
  2. 取り出したHeader , Payloadを(.)で繋いだ「一つの文字列」を作成する。
  3. Headerのalg を確認します
  4. サーバーが保持している秘密鍵(HS256:共通鍵暗号方式の場合)を用意する。
  5. 作成時と同じアルゴリズムを使い、手元の鍵で再度署名を計算する。
    計算式: HMACSHA256(受け取ったHeader + "." + 受け取ったPayload, サーバーの秘密鍵)
  6. 署名の照合(比較)
    文字列で比較する場合:
    再計算した署名を BASE64URL形式に変換し、JWTの第3セクションの文字列と突き合わせる。
    バイナリで比較する場合:
    JWTの第3セクションをデコードしてバイナリに戻し、再計算した署名の結果と 「1ビット(1バイト)でも違うか」を確認する。

3.有効期限(exp)の確認
署名が正しくても、期限切れのJWTは無効です。ペイロードをデコードし、exp クレームを確認します。現在時刻が exp を過ぎていれば、有効期限切れとして拒否します。

4.その他のクレーム検証
必要に応じて、以下の内容もチェックします。
iss (Issuer): 発行者が自分のシステムであるか。
aud (Audience): そのJWTが自分のサービス宛に発行されたものか。

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

認可については別途記事の作成を予定しています。

(参考)

JSON Web Tokenの概要
https://www.jwt.io/ja/introduction#what-is-json-web-token

コメント

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