トークンベース認証の代表格であるJWT(JSON Web Token)は、現代のWeb開発において欠かせない技術です。前回の記事で認証と認可の基礎を学びましたが、今回はJWTの内部構造に踏み込み、トークンがどのように生成され、検証されるのかを詳しく解説します。

JWTは一見すると意味不明な文字列に見えますが、その構造を理解すれば、トークンの中身を読み解き、セキュリティ上の問題点を発見できるようになります。この記事を読み終えるころには、JWTの構造を正確に説明でき、開発者ツールやコマンドラインでトークンを確認・検証できるスキルが身についているでしょう。

JWTとは何か

JWT(JSON Web Token、発音は「ジョット」)は、RFC 7519で標準化されたトークン形式です。2者間で情報を安全にやり取りするための、コンパクトでURLセーフな手段を提供します。

JWTの特徴

JWTには以下の特徴があります。

  • 自己完結型(Self-contained): トークン自体に必要な情報がすべて含まれているため、サーバー側でセッション情報を保持する必要がありません
  • コンパクト: Base64URLエンコードされた形式で、HTTPヘッダーやURLパラメータに含めやすいサイズです
  • 署名付き: デジタル署名により、トークンが改ざんされていないことを検証できます
  • 標準化: RFC 7519として標準化されており、多くのプログラミング言語でライブラリが提供されています

JWTが利用される場面

JWTは主に以下のシーンで活用されています。

  • 認証(Authentication): ログイン後にJWTを発行し、以降のリクエストでユーザーを識別します
  • 認可(Authorization): トークンに権限情報を含め、APIへのアクセス制御に使用します
  • 情報交換: 署名によって送信者の正当性を確認しながら、システム間で安全に情報をやり取りします

JWTの構造を理解する

JWTは3つのパートから構成され、それぞれがピリオド(.)で区切られています。

1
xxxxx.yyyyy.zzzzz

具体的なJWTの例を見てみましょう。

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

この一見ランダムな文字列は、以下の3つのパートで構成されています。

flowchart LR
    subgraph JWT["JWT構造"]
        direction LR
        H["Header<br>eyJhbGci..."] 
        P["Payload<br>eyJzdWIi..."]
        S["Signature<br>SflKxwRJ..."]
    end
    H --- dot1["."] --- P --- dot2["."] --- S
パート 内容
Header アルゴリズムとトークンタイプ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload クレーム情報 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ...
Signature 署名データ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWTの3つのパートを図解

flowchart TD
    subgraph JWT["完成したJWT"]
        direction TB
        A["Header<br>アルゴリズム<br>トークンタイプ"] 
        B["Payload<br>クレーム情報<br>ユーザー情報<br>有効期限など"]
        C["Signature<br>改ざん検知用<br>署名データ"]
    end
    A -->|Base64URLエンコード| D[".で連結"]
    B -->|Base64URLエンコード| D
    C -->|Base64URLエンコード| D
    D --> E["xxxxx.yyyyy.zzzzz"]

それでは、各パートの詳細を見ていきましょう。

Header(ヘッダー)の構造

Headerはトークンのメタ情報を格納する部分です。JSONオブジェクトをBase64URLエンコードして生成されます。

Headerの構成要素

Headerには通常、以下の2つの情報が含まれます。

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}
フィールド 説明
alg 署名に使用するアルゴリズム HS256, RS256, ES256
typ トークンのタイプ JWT

署名アルゴリズムの種類

JWTで使用される主要な署名アルゴリズムは以下の通りです。

アルゴリズム 種類 説明
HS256 共通鍵暗号 HMAC + SHA-256。送信者と受信者が同じ秘密鍵を共有
HS384 共通鍵暗号 HMAC + SHA-384
HS512 共通鍵暗号 HMAC + SHA-512
RS256 公開鍵暗号 RSA + SHA-256。秘密鍵で署名し、公開鍵で検証
RS384 公開鍵暗号 RSA + SHA-384
RS512 公開鍵暗号 RSA + SHA-512
ES256 公開鍵暗号 ECDSA + P-256曲線 + SHA-256
ES384 公開鍵暗号 ECDSA + P-384曲線 + SHA-384
ES512 公開鍵暗号 ECDSA + P-521曲線 + SHA-512
none なし 署名なし(セキュリティ上非推奨)

HeaderのBase64URLエンコード

上記のJSONをBase64URLエンコードすると、以下の文字列になります。

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Base64URLエンコードは、通常のBase64エンコードの出力から+-に、/_に置換し、パディングの=を除去したものです。これによりURLセーフな文字列が得られます。

Payload(ペイロード)の構造

Payloadはトークンの本体であり、「クレーム(Claims)」と呼ばれる情報を格納します。クレームとは、エンティティ(通常はユーザー)に関する情報や、トークン自体に関するメタデータです。

クレームの種類

クレームは3つのカテゴリに分類されます。

1. 登録済みクレーム(Registered Claims)

RFC 7519で事前に定義されたクレームです。使用は任意ですが、相互運用性を高めるために推奨されます。

クレーム 名前 説明
iss Issuer トークンの発行者
sub Subject トークンの主題(通常はユーザーID)
aud Audience トークンの受信者(対象者)
exp Expiration Time トークンの有効期限(UNIXタイムスタンプ)
nbf Not Before トークンが有効になる時刻
iat Issued At トークンの発行時刻
jti JWT ID トークンの一意な識別子

クレーム名が3文字と短いのは、JWTをできるだけコンパクトに保つためです。

2. パブリッククレーム(Public Claims)

IANA JSON Web Token Claimsレジストリに登録されたクレーム、またはURIを含む衝突耐性のある名前で定義されたクレームです。

1
2
3
{
  "https://example.com/is_admin": true
}

3. プライベートクレーム(Private Claims)

アプリケーション固有のカスタムクレームです。送信者と受信者の間で合意された独自の情報を格納します。

1
2
3
4
5
{
  "user_id": "12345",
  "role": "admin",
  "department": "engineering"
}

Payloadの例

実際のPayloadは以下のようになります。

1
2
3
4
5
6
7
8
9
{
  "iss": "https://example.com",
  "sub": "1234567890",
  "aud": "https://api.example.com",
  "exp": 1735689600,
  "iat": 1735686000,
  "name": "John Doe",
  "role": "admin"
}

このJSONをBase64URLエンコードすると、JWTの2番目のパートになります。

Payloadに関する重要な注意点

署名付きJWTであっても、Payloadの内容は誰でも読むことができます。Base64URLエンコードは暗号化ではなく、単なるエンコードです。そのため、パスワードやクレジットカード番号などの機密情報をPayloadに含めてはいけません。

警告: JWTのPayloadに機密情報を格納しないでください

格納可否
NG パスワード、クレジットカード番号、個人情報
OK ユーザーID、権限情報、トークンのメタデータ

Signature(署名)の構造

Signatureは、トークンが改ざんされていないことを保証するための部分です。Header、Payload、そして秘密鍵(またはキーペア)を使用して生成されます。

署名の生成プロセス

署名は以下の手順で生成されます。

  1. HeaderをBase64URLエンコード
  2. PayloadをBase64URLエンコード
  3. エンコードしたHeaderとPayloadをピリオドで連結
  4. 連結した文字列を、指定されたアルゴリズムと秘密鍵で署名
  5. 署名結果をBase64URLエンコード

HMAC SHA-256アルゴリズム(HS256)の場合、署名は以下の計算で生成されます。

1
2
3
4
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

署名生成の図解

flowchart TD
    subgraph Input["入力"]
        H["Header<br>{alg:HS256, typ:JWT}"]
        P["Payload<br>{sub:123, name:John}"]
    end
    H -->|Base64URLEncode| HE["eyJhbGci..."]
    P -->|Base64URLEncode| PE["eyJzdWIi..."]
    HE --> C["eyJhbGci... . eyJzdWIi..."]
    PE --> C
    C --> HMAC["HMACSHA256<br>data + secret_key"]
    HMAC --> BIN["署名結果<br>バイナリ"]
    BIN -->|Base64URLEncode| SIG["SflKxwRJSMeKKF2QT4..."]

共通鍵方式と公開鍵方式の違い

署名方式には大きく分けて2つのアプローチがあります。

共通鍵方式(HS256など)

flowchart LR
    subgraph Issuer["発行側"]
        IS["秘密鍵で署名"]
    end
    subgraph Verifier["検証側"]
        VE["秘密鍵で検証"]
    end
    Issuer <-->|同じ秘密鍵を共有| Verifier

公開鍵方式(RS256など)

flowchart LR
    subgraph Issuer["発行側"]
        IS["秘密鍵で署名"]
    end
    subgraph Verifier["検証側"]
        VE["公開鍵で検証"]
    end
    Issuer -->|公開鍵を配布| Verifier

公開鍵方式は、秘密鍵を検証側と共有する必要がないため、マイクロサービスアーキテクチャやサードパーティとの連携に適しています。

JWTのエンコードと署名の仕組み

ここまでの内容を踏まえ、JWTが生成される全体のプロセスを確認しましょう。

JWTエンコードの完全なフロー

入力データ

項目
Header JSON {"alg":"HS256","typ":"JWT"}
Payload JSON {"sub":"1234567890","name":"John Doe","iat":1516239022}
Secret Key your-256-bit-secret

処理フロー

flowchart TD
    subgraph Step1["Step 1: Header エンコード"]
        H1["{alg:HS256, typ:JWT}"] -->|Base64URL| H2["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"]
    end
    subgraph Step2["Step 2: Payload エンコード"]
        P1["{sub:1234567890, name:John Doe, ...}"] -->|Base64URL| P2["eyJzdWIiOiIxMjM0NTY3ODkwIi..."]
    end
    subgraph Step3["Step 3: 署名対象作成"]
        H2 --> CONCAT["Header.Payload"]
        P2 --> CONCAT
    end
    subgraph Step4["Step 4: 署名生成"]
        CONCAT -->|HMAC-SHA256| SIG["SflKxwRJSMeKKF2QT4..."]
    end
    subgraph Step5["Step 5: JWT完成"]
        SIG --> JWT["Header.Payload.Signature"]
    end

出力

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Base64URLエンコードの仕組み

Base64URLエンコードは、バイナリデータを安全にテキストに変換する方法です。

項目 Base64 Base64URL
使用文字 A-Z, a-z, 0-9, +, / A-Z, a-z, 0-9, -, _
パディング = なし
変換例 Hello+World/Test== Hello-World_Test

この変換により、JWTはURLのクエリパラメータやHTTPヘッダーに安全に含めることができます。

JWTの検証プロセス

JWTを受け取った側は、トークンが有効かどうかを検証する必要があります。検証プロセスは複数のステップで構成されます。

検証の全体フロー

flowchart TD
    START["JWTを受信"] --> STEP1
    subgraph STEP1["Step 1: 構造の検証"]
        S1A["ピリオドで3つに分割可能か"]
        S1B["各パートが空でないか"]
    end
    STEP1 --> STEP2
    subgraph STEP2["Step 2: Headerのデコード"]
        S2A["Base64URLデコード"]
        S2B["有効なJSONか"]
        S2C["algフィールドが存在するか"]
    end
    STEP2 --> STEP3
    subgraph STEP3["Step 3: 署名の検証"]
        S3A["Header.Payloadを再計算"]
        S3B["秘密鍵/公開鍵で署名検証"]
        S3C["署名が一致するか"]
    end
    STEP3 --> STEP4
    subgraph STEP4["Step 4: クレームの検証"]
        S4A["exp: 有効期限チェック"]
        S4B["nbf: 有効開始時刻チェック"]
        S4C["iss: 発行者チェック"]
        S4D["aud: 対象者チェック"]
    end
    STEP4 --> END["検証完了<br>トークン有効/無効"]

署名検証の詳細

署名検証は、トークンが改ざんされていないことを確認する最も重要なステップです。

受信したJWT: header.payload.signature

検証手順

  1. headerとpayloadを取り出す
  2. header + “.” + payload で署名対象を再構成
  3. 同じアルゴリズムと鍵で署名を再計算
  4. 再計算した署名と受信した署名を比較
flowchart TD
    R["受信した署名"] --> CMP{"比較"}
    C["再計算した署名"] --> CMP
    CMP -->|一致| VALID["有効"]
    CMP -->|不一致| INVALID["無効"]

クレーム検証のポイント

署名が正しくても、クレームの検証が不十分だとセキュリティ上の問題が生じます。

必須の検証項目

クレーム 検証内容 備考
exp(有効期限) 現在時刻 < exp であること 多少のクロックスキュー(数分程度)は許容することが多い
iat(発行時刻) 現在時刻 > iat であること 極端に未来の発行時刻は拒否
nbf(有効開始時刻) 現在時刻 >= nbf であること -
iss(発行者) 期待する発行者と一致すること -
aud(対象者) 自身のサービスが対象者に含まれていること -

JWTの実際の利用例

JWTがどのように使われるか、具体的なシナリオを見てみましょう。

API認証でのJWT利用フロー

sequenceDiagram
    participant U as ユーザー
    participant A as 認証サーバー
    participant API as APIサーバー
    
    U->>A: 1. ログイン要求<br>(email, password)
    A->>U: 2. 認証成功 → JWT発行
    U->>API: 3. APIリクエスト + JWT<br>(Authorization: Bearer JWT)
    Note over API: 4. JWT検証<br>- 署名検証<br>- クレーム検証
    API->>U: 5. レスポンス

HTTPリクエストでのJWT送信

JWTは通常、HTTPヘッダーのAuthorizationフィールドでBearerスキームを使用して送信します。

1
2
3
4
GET /api/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Content-Type: application/json

JWTを含むレスポンスの例

ログイン成功時に返されるレスポンスの例です。

1
2
3
4
5
6
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
}

開発者ツールでJWTを確認する方法

実際の開発では、JWTの内容を確認したり、デバッグしたりする場面が頻繁にあります。

ブラウザの開発者ツールで確認する

ChromeやFirefoxの開発者ツールを使用して、HTTPリクエストに含まれるJWTを確認できます。

  1. 開発者ツールを開く(F12またはCtrl+Shift+I)
  2. Networkタブを選択
  3. APIリクエストを選択
  4. HeadersセクションでAuthorizationヘッダーを確認
1
2
3
4
開発者ツールでの表示例:

Request Headers:
  Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

jwt.ioでJWTをデコードする

jwt.ioのデバッガーを使用すると、JWTの内容をWeb上で簡単に確認できます。

  1. https://jwt.io にアクセス
  2. Debuggerセクションにトークンを貼り付け
  3. Header、Payload、署名の検証結果が表示される

コマンドラインでJWTを確認する

bashとjqを使用して、ターミナルからJWTをデコードできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# JWTをデコードする関数
jwt_decode() {
  local jwt=$1
  # ヘッダーをデコード
  echo "=== Header ==="
  echo $jwt | cut -d'.' -f1 | base64 -d 2>/dev/null | jq .
  # ペイロードをデコード
  echo "=== Payload ==="
  echo $jwt | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
}

# 使用例
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
jwt_decode $TOKEN

実行結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
=== Header ===
{
  "alg": "HS256",
  "typ": "JWT"
}
=== Payload ===
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Node.jsでJWTを検証する

Node.jsのjsonwebtokenライブラリを使用した検証例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const jwt = require('jsonwebtoken');

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const secretKey = 'your-256-bit-secret';

try {
  // トークンを検証してデコード
  const decoded = jwt.verify(token, secretKey);
  console.log('検証成功:', decoded);
} catch (error) {
  if (error.name === 'TokenExpiredError') {
    console.error('トークンの有効期限が切れています');
  } else if (error.name === 'JsonWebTokenError') {
    console.error('無効なトークンです:', error.message);
  }
}

PythonでJWTを検証する

PythonのPyJWTライブラリを使用した検証例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import jwt

token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
secret_key = 'your-256-bit-secret'

try:
    # トークンを検証してデコード
    decoded = jwt.decode(token, secret_key, algorithms=['HS256'])
    print('検証成功:', decoded)
except jwt.ExpiredSignatureError:
    print('トークンの有効期限が切れています')
except jwt.InvalidTokenError as e:
    print('無効なトークンです:', str(e))

JWTを使用する際の注意点

JWTを安全に使用するために、いくつかの重要な注意点があります。

セキュリティ上の注意点

No 注意点 詳細
1 機密情報をPayloadに含めない Payloadは暗号化されておらず、Base64デコードで誰でも読める
2 適切な有効期限を設定する Access Tokenは短め(15分〜1時間程度)、Refresh Tokenで更新する仕組みを併用
3 algヘッダーを検証時に信頼しない サーバー側で許可するアルゴリズムを明示的に指定し、“none"アルゴリズムを拒否
4 秘密鍵を安全に管理する 環境変数やシークレット管理サービスを使用し、ソースコードにハードコードしない
5 HTTPSを使用する トークンが傍受されるリスクを軽減

“alg”: “none"攻撃への対策

署名アルゴリズムを「none」に変更する攻撃が知られています。

攻撃者が送信する悪意あるJWT

1
2
3
4
{
  "alg": "none",
  "typ": "JWT"
}

対策

対策 説明
アルゴリズムの明示的なリスト化 サーバー側で許可するアルゴリズムを指定
“none"の拒否 “none"アルゴリズムは常に拒否する
ライブラリ設定 ライブラリの設定で無効化する

Node.jsでの対策例:

1
2
3
4
// 許可するアルゴリズムを明示的に指定
const decoded = jwt.verify(token, secretKey, {
  algorithms: ['HS256']  // HS256のみ許可
});

まとめ

この記事では、JWTの構造と仕組みについて詳しく解説しました。

JWTの重要なポイントを振り返りましょう。

  • JWTの構造: Header、Payload、Signatureの3つのパートで構成され、ピリオドで連結されます
  • エンコード: 各パートはBase64URLエンコードされ、URLセーフな形式になります
  • 署名の仕組み: HeaderとPayloadを連結し、秘密鍵で署名することで改ざんを検知できます
  • 検証プロセス: 構造の検証、署名の検証、クレームの検証を順番に行います
  • 利用シーン: API認証、認可、システム間の安全な情報交換に活用されます

JWTはステートレスな認証を実現する強力なツールですが、適切に使用しないとセキュリティリスクが生じます。次回の記事では、Access TokenとRefresh Tokenを組み合わせた認証フローの実装について解説します。

参考リンク