JWT

JWT (JSON Web Token) 包括的技術ガイド

目次

  1. JWTとは何か
  2. JWTの構造
  3. ヘッダーフィールド
  4. ペイロードとクレーム
  5. 署名の生成
  6. 署名アルゴリズム
  7. 認証フロー
  8. 実装例(Python)
  9. JWE(JSON Web Encryption)
  10. Istio/Envoy JWT検証
  11. セキュリティベストプラクティス
  12. 一般的な脆弱性と攻撃

1. JWTとは何か

1.1 概要

JWT(JSON Web Token)は、RFC 7519で定義された、二者間で情報を安全にやり取りするためのコンパクトでURLセーフなトークン形式である。JWTは以下の特徴を持つ。

  • 自己完結型(Self-contained): トークン自体にユーザー情報や権限情報を含むため、サーバー側でセッション状態を保持する必要がない
  • コンパクト(Compact): Base64URLエンコーディングにより、URL、HTTPヘッダー、POSTパラメータ等で容易に伝送可能
  • 改ざん検知(Tamper Detection): デジタル署名により、トークンの内容が改ざんされていないことを検証可能
  • 標準化: IETF(Internet Engineering Task Force)により標準化されており、多くの言語・フレームワークでライブラリが提供されている

1.2 JWTが解決する課題

従来のセッションベース認証では、サーバー側にセッション情報を保持する必要があった。これはマイクロサービスアーキテクチャや分散システムにおいて以下の課題を引き起こす。

【従来のセッションベース認証の課題】

┌──────────┐     ┌──────────────┐     ┌──────────────┐
│  Client  │────>│  API Gateway │────>│  Service A   │
└──────────┘     └──────────────┘     └──────────────┘
                        │                     │
                        │  セッションID        │ セッション情報の
                        │  の転送              │ 共有が必要
                        │                     │
                 ┌──────────────┐     ┌──────────────┐
                 │ Session Store│<────│  Service B   │
                 │  (Redis等)   │     └──────────────┘
                 └──────────────┘
                        ^
                        │ 全サービスが共有ストアに
                        │ 依存 → 単一障害点(SPOF)
                        │ スケーラビリティの制約

JWTベースの認証では、トークン自体に必要な情報が含まれるため、共有セッションストアが不要になる。

【JWTベース認証のアーキテクチャ】

┌──────────┐     ┌──────────────┐     ┌──────────────┐
│  Client  │────>│  API Gateway │────>│  Service A   │
│          │     │  JWT検証     │     │  JWT検証     │
└──────────┘     └──────────────┘     └──────────────┘
     │                                       │
     │  Authorization:                       │ 公開鍵のみで
     │  Bearer <JWT>                         │ 独立して検証可能
     │                                       │
     │           ┌──────────────┐     ┌──────────────┐
     └──────────>│  Service B   │     │  JWKS        │
                 │  JWT検証     │<────│  Endpoint    │
                 └──────────────┘     │  (公開鍵配布) │
                                      └──────────────┘

1.3 JWTのユースケース

ユースケース説明
認証(Authentication)ユーザーログイン後、JWTを発行し、以後のリクエストで認証情報として使用
認可(Authorization)トークン内にロール・権限情報を含め、アクセス制御に利用
情報交換(Information Exchange)署名により改ざん検知が可能なため、安全な情報交換に利用
フェデレーション認証OAuth 2.0 / OpenID Connect (OIDC) でのIDトークンとして利用
サービス間認証マイクロサービス間の認証・認可に利用

1.4 関連するRFC仕様

RFC名称説明
RFC 7519JSON Web Token (JWT)JWTの基本仕様
RFC 7515JSON Web Signature (JWS)JWSの仕様(署名付きJWT)
RFC 7516JSON Web Encryption (JWE)JWEの仕様(暗号化JWT)
RFC 7517JSON Web Key (JWK)暗号鍵のJSON表現
RFC 7518JSON Web Algorithms (JWA)JWS/JWEで使用するアルゴリズムの仕様
RFC 7797JWS Unencoded Payload OptionペイロードをBase64URLエンコードしないオプション

2. JWTの構造

2.1 基本構造

JWTは3つのパートがドット(.)で連結された文字列である。

[ヘッダー].[ペイロード].[署名]
(Header) .(Payload)  .(Signature)

各パートはBase64URLエンコードされている。

JWTの構造図:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0xIn0
 ↑ ヘッダー(Base64URL)
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTYyNDI2MjIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsInJvbGVzIjpbImFkbWluIiwidXNlciJdfQ
 ↑ ペイロード(Base64URL)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
 ↑ 署名(Base64URL)

2.2 Base64URLエンコーディング

Base64URLは標準のBase64とは異なり、URLセーフな文字のみを使用する。

項目Base64Base64URL
62番目の文字+-
63番目の文字/_
パディング= を使用= を省略
import base64
import json

# ヘッダーの例
header = {
    "alg": "RS256",
    "typ": "JWT",
    "kid": "key-1"
}

# JSON → バイト列 → Base64URLエンコード
header_json = json.dumps(header, separators=(',', ':')).encode('utf-8')
header_b64 = base64.urlsafe_b64encode(header_json).rstrip(b'=').decode('utf-8')

print(f"JSON:      {header_json}")
print(f"Base64URL: {header_b64}")
# 出力:
# JSON:      b'{"alg":"RS256","typ":"JWT","kid":"key-1"}'
# Base64URL: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0xIn0

2.3 完全なJWT例

以下は実際のJWTを分解した例である。

完全なJWTトークン:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0xIn0.eyJzdWIiOiJ1c2VyLTEyMyIsIm5hbWUiOiLlsbHnlLDlpKrpg44iLCJyb2xlcyI6WyJhZG1pbiIsInNyZSJdLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlhdCI6MTcxNjIzOTAyMiwiZXhwIjoxNzE2MjQyNjIyLCJqdGkiOiJ1dWlkLTQ1NiJ9.SIGNATURE_HERE

デコード結果:

// ヘッダー (Header)
{
    "alg": "RS256",      // 署名アルゴリズム
    "typ": "JWT",        // トークンタイプ
    "kid": "key-1"       // 鍵識別子
}

// ペイロード (Payload)
{
    "sub": "user-123",                       // Subject(主体)
    "name": "山田太郎",                       // カスタムクレーム
    "roles": ["admin", "sre"],               // カスタムクレーム
    "iss": "https://auth.example.com",       // Issuer(発行者)
    "aud": "https://api.example.com",        // Audience(対象者)
    "iat": 1716239022,                       // Issued At(発行時刻)
    "exp": 1716242622,                       // Expiration(有効期限)
    "jti": "uuid-456"                        // JWT ID(一意識別子)
}

// 署名 (Signature)
// RSA-SHA256(Base64URL(Header) + "." + Base64URL(Payload), PrivateKey)

2.4 エンコード/デコードの流れ

【JWTエンコードの流れ】

  Header (JSON)          Payload (JSON)
       │                       │
       ▼                       ▼
  JSON.stringify          JSON.stringify
       │                       │
       ▼                       ▼
  Base64URL encode        Base64URL encode
       │                       │
       ▼                       ▼
  encoded_header          encoded_payload
       │                       │
       └───────┬───────────────┘
               │
               ▼
  signing_input = encoded_header + "." + encoded_payload
               │
               ▼
  signature = Sign(signing_input, secret_or_private_key)
               │
               ▼
  encoded_signature = Base64URL(signature)
               │
               ▼
  JWT = encoded_header + "." + encoded_payload + "." + encoded_signature
【JWTデコード・検証の流れ】

  JWT Token
       │
       ▼
  Split by "." → [encoded_header, encoded_payload, encoded_signature]
       │
       ├──────────────────────────────────┐
       │                                  │
       ▼                                  ▼
  Base64URL decode header            Base64URL decode payload
       │                                  │
       ▼                                  ▼
  Parse JSON → Header               Parse JSON → Payload
       │                                  │
       ├─ alg: "RS256"                    ├─ exp: 有効期限チェック
       ├─ typ: "JWT"                      ├─ nbf: 有効開始時刻チェック
       └─ kid: "key-1"                    ├─ iss: 発行者検証
               │                          ├─ aud: 対象者検証
               ▼                          └─ その他クレーム検証
  Verify signature:
  encoded_header + "." + encoded_payload
  を署名アルゴリズムと公開鍵で検証
               │
               ▼
     ┌─────────────────┐
     │  署名有効?       │
     │  クレーム有効?    │
     └─────────────────┘
        │           │
       Yes         No
        │           │
        ▼           ▼
     トークン有効   トークン拒否

3. ヘッダーフィールド

3.1 JOSE(JSON Object Signing and Encryption)ヘッダー

JWTのヘッダーはJOSEヘッダーとも呼ばれ、トークンのメタ情報を含む。

3.2 主要なヘッダーフィールド

フィールド名称必須説明
algAlgorithmYes署名または暗号化に使用するアルゴリズム
typTypeNo(推奨)トークンのタイプ(通常 "JWT"
kidKey IDNo(推奨)署名検証に使用する鍵の識別子
jkuJWK Set URLNoJWK Setを取得するURL
jwkJSON Web KeyNo署名検証用の公開鍵(JWK形式)
x5uX.509 URLNoX.509証明書チェーンのURL
x5cX.509 Certificate ChainNoX.509証明書チェーン
x5tX.509 Certificate SHA-1 ThumbprintNoX.509証明書のSHA-1サムプリント
x5t#S256X.509 Certificate SHA-256 ThumbprintNoX.509証明書のSHA-256サムプリント
ctyContent TypeNoペイロードのコンテンツタイプ(ネストJWTの場合 "JWT"
critCriticalNo処理必須のヘッダーパラメータ一覧

3.3 alg(Algorithm)フィールド

// 対称鍵アルゴリズム(HMAC)
{"alg": "HS256", "typ": "JWT"}

// 非対称鍵アルゴリズム(RSA)
{"alg": "RS256", "typ": "JWT"}

// 非対称鍵アルゴリズム(ECDSA)
{"alg": "ES256", "typ": "JWT"}

// 署名なし(セキュリティ上、本番環境では使用禁止)
{"alg": "none"}

3.4 kid(Key ID)フィールド

kidは鍵ローテーション時に特に重要である。複数の鍵が存在する場合、どの鍵で署名されたかを特定する。

{
    "alg": "RS256",
    "typ": "JWT",
    "kid": "2024-01-signing-key"
}
【kidを使った鍵の選択フロー】

  JWT受信
     │
     ▼
  ヘッダーからkidを取得
  kid = "2024-01-signing-key"
     │
     ▼
  JWKS Endpointから鍵セットを取得
  GET https://auth.example.com/.well-known/jwks.json
     │
     ▼
  ┌─────────────────────────────────────────┐
  │  JWKS Response:                         │
  │  {                                      │
  │    "keys": [                            │
  │      {"kid": "2023-12-signing-key",...}, │ ← 旧鍵
  │      {"kid": "2024-01-signing-key",...}, │ ← マッチ!
  │      {"kid": "2024-02-signing-key",...}  │ ← 次期鍵
  │    ]                                    │
  │  }                                      │
  └─────────────────────────────────────────┘
     │
     ▼
  kid="2024-01-signing-key" の公開鍵で署名検証

3.5 JWKS(JSON Web Key Set)の構造

{
    "keys": [
        {
            "kty": "RSA",
            "kid": "2024-01-signing-key",
            "use": "sig",
            "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM...",
            "e": "AQAB",
            "alg": "RS256"
        },
        {
            "kty": "EC",
            "kid": "2024-01-ec-key",
            "use": "sig",
            "crv": "P-256",
            "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
            "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
            "alg": "ES256"
        }
    ]
}
JWKフィールド説明
kty鍵タイプ(RSA, EC, OKP, oct)
kid鍵識別子
use用途(sig = 署名, enc = 暗号化)
key_ops鍵操作(sign, verify, encrypt, decrypt等)
alg使用アルゴリズム
n, eRSA公開鍵パラメータ(モジュラス、指数)
crv, x, yEC公開鍵パラメータ(曲線、座標)

4. ペイロードとクレーム

4.1 クレームの種類

JWTのペイロードには「クレーム(Claims)」と呼ばれるキー・バリューペアが含まれる。クレームは3種類に分類される。

┌─────────────────────────────────────────────────────────┐
│                   JWT Claims                             │
├─────────────────┬──────────────────┬────────────────────┤
│  Registered      │  Public          │  Private           │
│  Claims          │  Claims          │  Claims            │
│  (登録済み)       │  (公開)           │  (プライベート)     │
│                  │                  │                    │
│  RFC 7519で定義   │  IANAレジストリ   │  当事者間で合意     │
│  共通理解を提供    │  に登録済み       │  した独自クレーム   │
│                  │                  │                    │
│  iss, sub, aud   │  email, name,    │  roles, tenant_id, │
│  exp, nbf, iat   │  profile,        │  permissions,      │
│  jti             │  email_verified  │  department        │
└─────────────────┴──────────────────┴────────────────────┘

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

クレーム名称説明
issIssuerStringトークンの発行者"https://auth.example.com"
subSubjectStringトークンの主体(通常はユーザーID)"user-123"
audAudienceString/Arrayトークンの対象者"https://api.example.com"
expExpiration TimeNumericDate有効期限(UNIXタイムスタンプ)1716242622
nbfNot BeforeNumericDate有効開始時刻1716239022
iatIssued AtNumericDate発行時刻1716239022
jtiJWT IDStringトークンの一意識別子(リプレイ攻撃防止)"550e8400-e29b-41d4-a716-446655440000"

4.3 各クレームの詳細

iss(Issuer) - 発行者

{
    "iss": "https://auth.example.com"
}

検証側では、期待する発行者と一致するかを確認する。これにより、信頼できない発行者からのトークンを拒否できる。

# 検証時のissチェック
decoded = jwt.decode(
    token,
    key=public_key,
    algorithms=["RS256"],
    issuer="https://auth.example.com"  # 期待するissuer
)
# issが一致しない場合、InvalidIssuerError が発生

sub(Subject) - 主体

{
    "sub": "user-123"
}

通常はユーザーIDやサービスアカウントIDを格納する。isssub の組み合わせがグローバルに一意であることが推奨される。

aud(Audience) - 対象者

// 単一の対象者
{
    "aud": "https://api.example.com"
}

// 複数の対象者
{
    "aud": [
        "https://api.example.com",
        "https://admin.example.com"
    ]
}

検証側では、自身が対象者に含まれているかを確認する。これにより、別のサービス向けに発行されたトークンの流用を防止できる。

exp(Expiration Time) - 有効期限

{
    "exp": 1716242622,
    "iat": 1716239022
}
// exp - iat = 3600秒(1時間)
import time

# 現在時刻と有効期限の比較
current_time = time.time()  # 例: 1716240000
exp = 1716242622

if current_time > exp:
    print("トークン期限切れ")
else:
    remaining = exp - current_time
    print(f"残り有効時間: {remaining:.0f}秒")

nbf(Not Before) - 有効開始時刻

{
    "nbf": 1716239022,
    "exp": 1716242622
}

nbfより前の時刻ではトークンは無効とみなされる。これは「予約トークン」のような使い方ができる。

jti(JWT ID) - 一意識別子

{
    "jti": "550e8400-e29b-41d4-a716-446655440000"
}

リプレイ攻撃防止に使用される。サーバー側で使用済みのjtiを記録しておき、同じjtiを持つトークンの再利用を防ぐ。

import uuid

# トークン発行時
payload = {
    "sub": "user-123",
    "jti": str(uuid.uuid4()),  # UUIDv4で一意性を保証
    "iat": int(time.time()),
    "exp": int(time.time()) + 3600
}

4.4 カスタムクレーム(Private Claims)

{
    "sub": "user-123",
    "iss": "https://auth.example.com",
    "exp": 1716242622,
    "iat": 1716239022,

    // --- カスタムクレーム ---
    "roles": ["admin", "sre"],
    "permissions": ["read:logs", "write:config", "admin:cluster"],
    "tenant_id": "org-456",
    "department": "Platform Engineering",
    "environment": "production"
}

カスタムクレーム設計のベストプラクティス:

  1. 名前空間の使用: 衝突を避けるため、URI形式の名前空間を使用する

    {
        "https://example.com/roles": ["admin"],
        "https://example.com/tenant": "org-456"
    }
    
  2. 最小限の情報: ペイロードに含める情報は必要最小限にする(JWTはBase64エンコードされるだけで暗号化されない)

  3. 機密情報の除外: パスワード、クレジットカード番号、個人情報等は含めない

4.5 NumericDate型について

JWT仕様でのNumericDate型は、UTCの1970年1月1日00:00:00からの経過秒数(UNIXタイムスタンプ)である。

from datetime import datetime, timezone

# 現在時刻のNumericDate
now = int(datetime.now(timezone.utc).timestamp())
print(f"現在: {now}")  # 例: 1716239022

# NumericDateから人間が読める形式へ
exp_timestamp = 1716242622
exp_datetime = datetime.fromtimestamp(exp_timestamp, tz=timezone.utc)
print(f"有効期限: {exp_datetime.isoformat()}")
# 出力: 2024-05-21T01:03:42+00:00

5. 署名の生成

5.1 署名の目的

JWTの署名は以下を保証する。

  • 完全性(Integrity): トークンの内容が改ざんされていないこと
  • 真正性(Authenticity): トークンが正当な発行者によって作成されたこと

注意: 署名は暗号化ではない。ペイロードはBase64URLデコードすれば誰でも読める。機密性が必要な場合はJWE(JSON Web Encryption)を使用する。

5.2 署名生成のプロセス

【署名生成プロセス(HMAC-SHA256の場合)】

  Step 1: 入力の準備
  ┌────────────────────────────────────────────┐
  │ Header:  {"alg":"HS256","typ":"JWT"}       │
  │ Payload: {"sub":"user-123","exp":167...}   │
  └────────────────────────────────────────────┘
            │                    │
            ▼                    ▼
  Step 2: Base64URLエンコード
  ┌─────────────────────┐ ┌──────────────────────┐
  │ eyJhbGciOiJIUzI1... │ │ eyJzdWIiOiJ1c2Vy... │
  │ (encoded_header)     │ │ (encoded_payload)    │
  └─────────────────────┘ └──────────────────────┘
            │                    │
            └──────┬─────────────┘
                   │
                   ▼
  Step 3: 署名入力の作成
  ┌────────────────────────────────────────────┐
  │ signing_input = encoded_header + "." +     │
  │                 encoded_payload             │
  └────────────────────────────────────────────┘
                   │
                   ▼
  Step 4: 署名計算
  ┌────────────────────────────────────────────┐
  │ signature = HMAC-SHA256(                   │
  │     key = shared_secret,                   │
  │     data = signing_input                   │
  │ )                                          │
  └────────────────────────────────────────────┘
                   │
                   ▼
  Step 5: Base64URLエンコード
  ┌────────────────────────────────────────────┐
  │ encoded_signature = Base64URL(signature)   │
  └────────────────────────────────────────────┘
                   │
                   ▼
  Step 6: JWT組み立て
  ┌────────────────────────────────────────────┐
  │ JWT = encoded_header + "." +               │
  │       encoded_payload + "." +              │
  │       encoded_signature                    │
  └────────────────────────────────────────────┘

5.3 対称鍵 vs 非対称鍵

【対称鍵署名(HMAC)】

  発行者                              検証者
  ┌─────────┐                        ┌─────────┐
  │         │  共有秘密鍵             │         │
  │  Sign   │◄────────────────────►  │ Verify  │
  │         │  (同じ鍵で署名・検証)    │         │
  └─────────┘                        └─────────┘

  メリット: 高速、実装が簡単
  デメリット: 鍵の安全な共有が必要、発行者と検証者が同一の鍵を持つ必要がある


【非対称鍵署名(RSA/ECDSA)】

  発行者                              検証者
  ┌─────────┐                        ┌─────────┐
  │         │  秘密鍵で署名           │         │
  │  Sign   │───────────────────────>│ Verify  │
  │         │           公開鍵で検証  │         │
  └─────────┘                        └─────────┘
       │                                  ▲
       │                                  │
  ┌─────────┐                     ┌──────────────┐
  │  秘密鍵  │                     │   公開鍵      │
  │ (発行者  │                     │ (JWKS endpoint│
  │  のみ保持)│                     │  で公開)      │
  └─────────┘                     └──────────────┘

  メリット: 秘密鍵は発行者のみ保持、検証者は公開鍵のみ必要
  デメリット: HMACより計算コストが高い

5.4 署名検証の擬似コード

import hmac
import hashlib
import base64
import json

def create_jwt_hmac256(payload: dict, secret: str) -> str:
    """HMAC-SHA256でJWTを作成する"""

    # ヘッダー
    header = {"alg": "HS256", "typ": "JWT"}

    # Base64URLエンコード
    def b64url_encode(data: bytes) -> str:
        return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

    encoded_header = b64url_encode(
        json.dumps(header, separators=(',', ':')).encode('utf-8')
    )
    encoded_payload = b64url_encode(
        json.dumps(payload, separators=(',', ':')).encode('utf-8')
    )

    # 署名入力
    signing_input = f"{encoded_header}.{encoded_payload}"

    # HMAC-SHA256署名
    signature = hmac.new(
        secret.encode('utf-8'),
        signing_input.encode('utf-8'),
        hashlib.sha256
    ).digest()

    encoded_signature = b64url_encode(signature)

    return f"{signing_input}.{encoded_signature}"


def verify_jwt_hmac256(token: str, secret: str) -> dict:
    """HMAC-SHA256でJWTを検証する"""

    def b64url_decode(data: str) -> bytes:
        padding = 4 - len(data) % 4
        data += '=' * padding
        return base64.urlsafe_b64decode(data)

    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError("不正なJWT形式")

    encoded_header, encoded_payload, encoded_signature = parts

    # 署名検証
    signing_input = f"{encoded_header}.{encoded_payload}"
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signing_input.encode('utf-8'),
        hashlib.sha256
    ).digest()

    actual_signature = b64url_decode(encoded_signature)

    if not hmac.compare_digest(expected_signature, actual_signature):
        raise ValueError("署名が無効です")

    # ペイロードのデコード
    payload = json.loads(b64url_decode(encoded_payload))

    # 有効期限の検証
    import time
    if 'exp' in payload and time.time() > payload['exp']:
        raise ValueError("トークンの有効期限が切れています")

    return payload


# 使用例
secret = "my-super-secret-key-at-least-256-bits-long!!"
payload = {
    "sub": "user-123",
    "name": "山田太郎",
    "iat": 1716239022,
    "exp": 1716242622
}

token = create_jwt_hmac256(payload, secret)
print(f"JWT: {token}")

verified_payload = verify_jwt_hmac256(token, secret)
print(f"Verified: {verified_payload}")

6. 署名アルゴリズム

6.1 アルゴリズム一覧

JWA(RFC 7518)で定義されている署名アルゴリズムは以下の通りである。

HMAC(Hash-based Message Authentication Code)

アルゴリズムハッシュ関数鍵タイプ鍵サイズ(最小)用途
HS256SHA-256対称鍵256 bits一般用途
HS384SHA-384対称鍵384 bits高セキュリティ
HS512SHA-512対称鍵512 bits最高セキュリティ
import jwt

# HS256の例
secret = "my-secret-key-must-be-at-least-256-bits-long-for-hs256!!"
token = jwt.encode(
    {"sub": "user-123", "exp": 1716242622},
    secret,
    algorithm="HS256"
)

RSA(RSASSA-PKCS1-v1_5)

アルゴリズムハッシュ関数鍵タイプ推奨鍵サイズ用途
RS256SHA-256RSA非対称鍵2048+ bits最も広く使用
RS384SHA-384RSA非対称鍵2048+ bits高セキュリティ
RS512SHA-512RSA非対称鍵2048+ bits最高セキュリティ
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# RSA鍵ペア生成
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)

# PEM形式でエクスポート
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

public_pem = private_key.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# RS256での署名
token = jwt.encode(
    {"sub": "user-123", "exp": 1716242622},
    private_pem,
    algorithm="RS256"
)

# RS256での検証
decoded = jwt.decode(
    token,
    public_pem,
    algorithms=["RS256"]
)

RSA-PSS(RSASSA-PSS)

アルゴリズムハッシュ関数鍵タイプ推奨鍵サイズ用途
PS256SHA-256RSA非対称鍵2048+ bitsRSAの改良版
PS384SHA-384RSA非対称鍵2048+ bits高セキュリティ
PS512SHA-512RSA非対称鍵2048+ bits最高セキュリティ

RSA-PSSはRSASSA-PKCS1-v1_5より安全とされ、確率的署名スキームを使用する。

# PS256での署名(同じRSA鍵ペアを使用)
token = jwt.encode(
    {"sub": "user-123", "exp": 1716242622},
    private_pem,
    algorithm="PS256"
)

ECDSA(Elliptic Curve Digital Signature Algorithm)

アルゴリズム曲線ハッシュ鍵サイズセキュリティ強度
ES256P-256SHA-256256 bitsRSA 3072 bits相当
ES384P-384SHA-384384 bitsRSA 7680 bits相当
ES512P-521SHA-512521 bitsRSA 15360 bits相当
from cryptography.hazmat.primitives.asymmetric import ec

# EC鍵ペア生成(P-256曲線)
ec_private_key = ec.generate_private_key(ec.SECP256R1())

ec_private_pem = ec_private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

ec_public_pem = ec_private_key.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# ES256での署名
token = jwt.encode(
    {"sub": "user-123", "exp": 1716242622},
    ec_private_pem,
    algorithm="ES256"
)

# ES256での検証
decoded = jwt.decode(
    token,
    ec_public_pem,
    algorithms=["ES256"]
)

EdDSA(Edwards-curve Digital Signature Algorithm)

アルゴリズム曲線鍵サイズ特徴
EdDSAEd25519256 bits高速、定数時間実装
EdDSAEd448448 bitsより高いセキュリティ
from cryptography.hazmat.primitives.asymmetric import ed25519

# Ed25519鍵ペア生成
ed_private_key = ed25519.Ed25519PrivateKey.generate()

ed_private_pem = ed_private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

ed_public_pem = ed_private_key.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# EdDSAでの署名
token = jwt.encode(
    {"sub": "user-123", "exp": 1716242622},
    ed_private_pem,
    algorithm="EdDSA"
)

noneアルゴリズム

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

警告: noneアルゴリズムは署名を行わない。本番環境では絶対に使用してはならない。これは重大なセキュリティ脆弱性の原因となる(後述の「alg:none攻撃」参照)。

6.2 アルゴリズム比較表

【アルゴリズム比較】

                   鍵管理    性能      署名サイズ   セキュリティ  推奨度
  HS256           ★★★★★   ★★★★★   32 bytes    ★★★        サービス内
  RS256           ★★★      ★★★      256 bytes   ★★★★      標準推奨
  PS256           ★★★      ★★★      256 bytes   ★★★★★    高セキュリティ
  ES256           ★★★★    ★★★★    64 bytes    ★★★★★    モバイル/IoT
  EdDSA(Ed25519)  ★★★★    ★★★★★   64 bytes    ★★★★★    最新推奨
  none            N/A       N/A       0 bytes     ★          使用禁止

  ★ = 低い/少ない  ★★★★★ = 高い/多い

6.3 アルゴリズム選定ガイド

【アルゴリズム選定フローチャート】

  Start
    │
    ▼
  発行者と検証者は同一サービスか?
    │
   Yes ──────────────────────────► HS256 (対称鍵)
    │                              シンプルで高速
   No
    │
    ▼
  RSAインフラが既存か?
    │
   Yes ──┐
    │    ▼
   No   PKCS1互換性が必要か?
    │    │
    │   Yes ──► RS256 (RSASSA-PKCS1)
    │    │
    │   No ───► PS256 (RSA-PSS)
    │           より安全な選択
    ▼
  鍵サイズを小さくしたいか?
  (モバイル/IoT/帯域制限)
    │
   Yes ──────────────────────────► ES256 (ECDSA)
    │                              小さい鍵で高セキュリティ
   No
    │
    ▼
  最新の標準を使いたいか?
    │
   Yes ──────────────────────────► EdDSA (Ed25519)
    │                              最速・最も安全
   No
    │
    ▼
  デフォルト推奨 ──────────────► RS256 or ES256
                                 互換性と安全性のバランス

7. 認証フロー

7.1 アクセストークンフロー

【JWT認証フロー(アクセストークン)】

  Client                Auth Server              API Server
    │                       │                        │
    │  1. ログインリクエスト   │                        │
    │  POST /auth/login     │                        │
    │  {username, password}  │                        │
    │──────────────────────>│                        │
    │                       │                        │
    │                       │ 2. 認証情報の検証        │
    │                       │    (DB/LDAP/OIDC)      │
    │                       │                        │
    │  3. JWTトークン返却     │                        │
    │  {                    │                        │
    │    access_token: JWT,  │                        │
    │    token_type: Bearer, │                        │
    │    expires_in: 3600    │                        │
    │  }                    │                        │
    │<──────────────────────│                        │
    │                       │                        │
    │  4. APIリクエスト                                │
    │  GET /api/resources                             │
    │  Authorization: Bearer <JWT>                    │
    │────────────────────────────────────────────────>│
    │                                                 │
    │                                5. JWT検証        │
    │                                - 署名検証        │
    │                                - exp確認         │
    │                                - iss/aud確認     │
    │                                - claims確認      │
    │                                                 │
    │  6. APIレスポンス                                 │
    │  200 OK                                         │
    │  {"data": [...]}                                │
    │<────────────────────────────────────────────────│
    │                                                 │
    │  7. トークン期限切れ後のリクエスト                   │
    │  GET /api/resources                             │
    │  Authorization: Bearer <expired JWT>            │
    │────────────────────────────────────────────────>│
    │                                                 │
    │  8. エラーレスポンス                               │
    │  401 Unauthorized                               │
    │  {"error": "token_expired"}                     │
    │<────────────────────────────────────────────────│

7.2 トークンリフレッシュフロー

【トークンリフレッシュフロー】

  Client                Auth Server              API Server
    │                       │                        │
    │  1. 初回ログイン        │                        │
    │  POST /auth/login     │                        │
    │──────────────────────>│                        │
    │                       │                        │
    │  2. トークンペア返却    │                        │
    │  {                    │                        │
    │    access_token: JWT,  │ (短寿命: 15分)         │
    │    refresh_token: JWT, │ (長寿命: 7日)          │
    │    expires_in: 900     │                        │
    │  }                    │                        │
    │<──────────────────────│                        │
    │                       │                        │
    │  3. APIリクエスト(access_tokenで)               │
    │  Authorization: Bearer <access_token>           │
    │────────────────────────────────────────────────>│
    │                                                 │
    │  4. 200 OK (正常応答)                            │
    │<────────────────────────────────────────────────│
    │                                                 │
    │  ... 15分経過 ...                                │
    │                                                 │
    │  5. APIリクエスト(期限切れ)                       │
    │  Authorization: Bearer <expired access_token>   │
    │────────────────────────────────────────────────>│
    │                                                 │
    │  6. 401 Unauthorized                            │
    │<────────────────────────────────────────────────│
    │                       │                        │
    │  7. トークンリフレッシュ │                        │
    │  POST /auth/refresh   │                        │
    │  {refresh_token: JWT}  │                        │
    │──────────────────────>│                        │
    │                       │                        │
    │                       │ 8. refresh_token検証    │
    │                       │    - 署名検証            │
    │                       │    - 有効期限確認        │
    │                       │    - ブラックリスト確認   │
    │                       │                        │
    │  9. 新しいトークンペア   │                        │
    │  {                    │                        │
    │    access_token: 新JWT,│                        │
    │    refresh_token: 新JWT│ (リフレッシュトークン    │
    │  }                    │  ローテーション)         │
    │<──────────────────────│                        │
    │                       │                        │
    │  10. 新access_tokenでAPIリクエスト                │
    │  Authorization: Bearer <new access_token>       │
    │────────────────────────────────────────────────>│
    │                                                 │
    │  11. 200 OK                                     │
    │<────────────────────────────────────────────────│

7.3 アクセストークン vs リフレッシュトークン

項目アクセストークンリフレッシュトークン
目的APIアクセスの認可新しいアクセストークンの取得
有効期間短い(5-60分)長い(数時間-数日)
保存場所メモリ(推奨)HttpOnly Cookie / セキュアストレージ
送信先リソースサーバー(API)認証サーバーのみ
含まれる情報ユーザーID、権限、スコープユーザーID、セッションID
漏洩時の影響短時間のみ有効長期間のアクセスが可能
無効化基本的に不可(有効期限まで有効)サーバー側でブラックリスト化可能
ローテーションリフレッシュ時に毎回再発行Refresh Token Rotationで毎回再発行

7.4 Refresh Token Rotation

【Refresh Token Rotation の仕組み】

  時刻 T0: ログイン
  ┌─────────────────────────────────┐
  │ access_token_1  (exp: T0+15min) │
  │ refresh_token_1 (exp: T0+7day)  │
  └─────────────────────────────────┘

  時刻 T1 (=T0+15min): access_token_1 期限切れ
  POST /auth/refresh {refresh_token_1}
  ┌─────────────────────────────────┐
  │ access_token_2  (exp: T1+15min) │
  │ refresh_token_2 (exp: T1+7day)  │  ← refresh_token_1は無効化
  └─────────────────────────────────┘

  時刻 T2 (=T1+15min): access_token_2 期限切れ
  POST /auth/refresh {refresh_token_2}
  ┌─────────────────────────────────┐
  │ access_token_3  (exp: T2+15min) │
  │ refresh_token_3 (exp: T2+7day)  │  ← refresh_token_2は無効化
  └─────────────────────────────────┘

  攻撃者がrefresh_token_1を盗んで使用した場合:
  POST /auth/refresh {refresh_token_1}
  → 既に無効化済み → 拒否 & 全トークン無効化(セキュリティイベント)

7.5 OAuth 2.0 + JWTフロー

【OAuth 2.0 Authorization Code Flow + JWT】

  User        Client App      Auth Server       Resource Server
   │              │                │                   │
   │ 1. アクセス   │                │                   │
   │─────────────>│                │                   │
   │              │                │                   │
   │ 2. 認可リダイレクト            │                   │
   │<─────────────│                │                   │
   │              │                │                   │
   │ 3. ログイン & 同意画面         │                   │
   │────────────────────────────>│                   │
   │              │                │                   │
   │ 4. 認可コード付きリダイレクト   │                   │
   │<────────────────────────────│                   │
   │              │                │                   │
   │ 5. 認可コード転送              │                   │
   │─────────────>│                │                   │
   │              │                │                   │
   │              │ 6. トークン交換  │                   │
   │              │ POST /token    │                   │
   │              │ {code, secret} │                   │
   │              │───────────────>│                   │
   │              │                │                   │
   │              │ 7. トークン返却  │                   │
   │              │ {access_token,  │                   │
   │              │  id_token(JWT), │                   │
   │              │  refresh_token} │                   │
   │              │<───────────────│                   │
   │              │                │                   │
   │              │ 8. API呼び出し                      │
   │              │ Authorization: Bearer <JWT>         │
   │              │────────────────────────────────────>│
   │              │                │                   │
   │              │ 9. レスポンス                        │
   │              │<────────────────────────────────────│
   │              │                │                   │
   │ 10. 結果表示  │                │                   │
   │<─────────────│                │                   │

8. 実装例(Python)

8.1 PyJWTによるJWT発行・検証(RSA鍵)

#!/usr/bin/env python3
"""
JWT発行・検証の完全な実装例(RSA鍵使用)
必要なパッケージ: pip install PyJWT cryptography
"""

import jwt
import time
import uuid
from datetime import datetime, timezone, timedelta
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization


# ============================================================
# 1. RSA鍵ペアの生成
# ============================================================

def generate_rsa_key_pair():
    """RSA 2048-bit鍵ペアを生成する"""
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048
    )

    private_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )

    public_pem = private_key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )

    return private_pem, public_pem


# ============================================================
# 2. JWT発行(トークン生成)
# ============================================================

class JWTIssuer:
    """JWT発行者クラス"""

    def __init__(self, private_key: bytes, issuer: str, key_id: str = "key-1"):
        self.private_key = private_key
        self.issuer = issuer
        self.key_id = key_id

    def issue_access_token(
        self,
        user_id: str,
        roles: list[str],
        audience: str,
        expires_in: int = 900  # 15分
    ) -> str:
        """アクセストークンを発行する"""
        now = datetime.now(timezone.utc)

        payload = {
            # 登録済みクレーム
            "iss": self.issuer,
            "sub": user_id,
            "aud": audience,
            "exp": now + timedelta(seconds=expires_in),
            "nbf": now,
            "iat": now,
            "jti": str(uuid.uuid4()),

            # カスタムクレーム
            "roles": roles,
            "token_type": "access"
        }

        headers = {
            "kid": self.key_id
        }

        return jwt.encode(
            payload,
            self.private_key,
            algorithm="RS256",
            headers=headers
        )

    def issue_refresh_token(
        self,
        user_id: str,
        session_id: str,
        expires_in: int = 604800  # 7日
    ) -> str:
        """リフレッシュトークンを発行する"""
        now = datetime.now(timezone.utc)

        payload = {
            "iss": self.issuer,
            "sub": user_id,
            "exp": now + timedelta(seconds=expires_in),
            "iat": now,
            "jti": str(uuid.uuid4()),
            "session_id": session_id,
            "token_type": "refresh"
        }

        headers = {
            "kid": self.key_id
        }

        return jwt.encode(
            payload,
            self.private_key,
            algorithm="RS256",
            headers=headers
        )

    def issue_token_pair(
        self,
        user_id: str,
        roles: list[str],
        audience: str
    ) -> dict:
        """アクセストークンとリフレッシュトークンのペアを発行する"""
        session_id = str(uuid.uuid4())

        access_token = self.issue_access_token(
            user_id=user_id,
            roles=roles,
            audience=audience
        )

        refresh_token = self.issue_refresh_token(
            user_id=user_id,
            session_id=session_id
        )

        return {
            "access_token": access_token,
            "refresh_token": refresh_token,
            "token_type": "Bearer",
            "expires_in": 900
        }


# ============================================================
# 3. JWT検証
# ============================================================

class JWTVerifier:
    """JWT検証者クラス"""

    def __init__(
        self,
        public_key: bytes,
        issuer: str,
        audience: str,
        algorithms: list[str] = None
    ):
        self.public_key = public_key
        self.issuer = issuer
        self.audience = audience
        self.algorithms = algorithms or ["RS256"]

    def verify_access_token(self, token: str) -> dict:
        """アクセストークンを検証する"""
        try:
            payload = jwt.decode(
                token,
                self.public_key,
                algorithms=self.algorithms,
                issuer=self.issuer,
                audience=self.audience,
                options={
                    "require": ["exp", "iss", "sub", "aud", "iat", "jti"],
                    "verify_exp": True,
                    "verify_nbf": True,
                    "verify_iss": True,
                    "verify_aud": True,
                }
            )

            # token_typeの検証
            if payload.get("token_type") != "access":
                raise jwt.InvalidTokenError(
                    "トークンタイプが不正です(accessが期待されます)"
                )

            return payload

        except jwt.ExpiredSignatureError:
            raise ValueError("トークンの有効期限が切れています")
        except jwt.InvalidIssuerError:
            raise ValueError("トークンの発行者が不正です")
        except jwt.InvalidAudienceError:
            raise ValueError("トークンの対象者が不正です")
        except jwt.InvalidSignatureError:
            raise ValueError("トークンの署名が不正です")
        except jwt.DecodeError as e:
            raise ValueError(f"トークンのデコードに失敗しました: {e}")

    def verify_refresh_token(self, token: str) -> dict:
        """リフレッシュトークンを検証する"""
        try:
            payload = jwt.decode(
                token,
                self.public_key,
                algorithms=self.algorithms,
                issuer=self.issuer,
                options={
                    "require": ["exp", "iss", "sub", "iat", "jti"],
                    "verify_exp": True,
                    "verify_iss": True,
                    "verify_aud": False,  # リフレッシュトークンにはaudなし
                }
            )

            if payload.get("token_type") != "refresh":
                raise jwt.InvalidTokenError(
                    "トークンタイプが不正です(refreshが期待されます)"
                )

            return payload

        except jwt.ExpiredSignatureError:
            raise ValueError("リフレッシュトークンの有効期限が切れています")
        except jwt.InvalidSignatureError:
            raise ValueError("リフレッシュトークンの署名が不正です")

    def get_unverified_header(self, token: str) -> dict:
        """検証前にヘッダーを取得する(kid取得用)"""
        return jwt.get_unverified_header(token)


# ============================================================
# 4. 使用例
# ============================================================

def main():
    # 鍵ペア生成
    private_pem, public_pem = generate_rsa_key_pair()

    # 発行者の初期化
    issuer = JWTIssuer(
        private_key=private_pem,
        issuer="https://auth.example.com",
        key_id="2024-01-key"
    )

    # 検証者の初期化
    verifier = JWTVerifier(
        public_key=public_pem,
        issuer="https://auth.example.com",
        audience="https://api.example.com"
    )

    # トークンペアの発行
    tokens = issuer.issue_token_pair(
        user_id="user-123",
        roles=["admin", "sre"],
        audience="https://api.example.com"
    )

    print("=== 発行されたトークン ===")
    print(f"Access Token:  {tokens['access_token'][:50]}...")
    print(f"Refresh Token: {tokens['refresh_token'][:50]}...")
    print(f"Token Type:    {tokens['token_type']}")
    print(f"Expires In:    {tokens['expires_in']}秒")

    # アクセストークンの検証
    payload = verifier.verify_access_token(tokens['access_token'])
    print("\n=== 検証結果 ===")
    print(f"User ID: {payload['sub']}")
    print(f"Roles:   {payload['roles']}")
    print(f"Issuer:  {payload['iss']}")
    print(f"JWT ID:  {payload['jti']}")

    # ヘッダーの確認
    header = verifier.get_unverified_header(tokens['access_token'])
    print(f"\n=== ヘッダー ===")
    print(f"Algorithm: {header['alg']}")
    print(f"Key ID:    {header['kid']}")


if __name__ == "__main__":
    main()

8.2 FastAPI JWT認証ミドルウェア(RBAC対応)

#!/usr/bin/env python3
"""
FastAPI JWT認証ミドルウェア(ロールベースアクセス制御対応)
必要なパッケージ: pip install fastapi uvicorn PyJWT cryptography
"""

from fastapi import FastAPI, HTTPException, Depends, Security, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.middleware.cors import CORSMiddleware
from functools import wraps
from typing import Optional
import jwt
import time
import logging

logger = logging.getLogger(__name__)

app = FastAPI(title="JWT Protected API", version="1.0.0")

# CORS設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

# セキュリティスキーム
security = HTTPBearer()


# ============================================================
# JWT設定
# ============================================================

class JWTConfig:
    """JWT検証設定"""
    ISSUER = "https://auth.example.com"
    AUDIENCE = "https://api.example.com"
    ALGORITHMS = ["RS256"]

    # 本番環境ではJWKS Endpointから取得
    # ここではデモ用にPEM形式の公開鍵を使用
    PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""


# ============================================================
# JWT検証の依存性注入
# ============================================================

class TokenPayload:
    """検証済みトークンペイロード"""

    def __init__(self, payload: dict):
        self.sub = payload.get("sub")
        self.roles = payload.get("roles", [])
        self.permissions = payload.get("permissions", [])
        self.iss = payload.get("iss")
        self.exp = payload.get("exp")
        self.jti = payload.get("jti")
        self.raw = payload

    @property
    def user_id(self) -> str:
        return self.sub

    def has_role(self, role: str) -> bool:
        return role in self.roles

    def has_permission(self, permission: str) -> bool:
        return permission in self.permissions

    def has_any_role(self, roles: list[str]) -> bool:
        return any(role in self.roles for role in roles)


async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Security(security)
) -> TokenPayload:
    """JWTを検証してユーザー情報を取得する"""
    token = credentials.credentials

    try:
        payload = jwt.decode(
            token,
            JWTConfig.PUBLIC_KEY,
            algorithms=JWTConfig.ALGORITHMS,
            issuer=JWTConfig.ISSUER,
            audience=JWTConfig.AUDIENCE,
            options={
                "require": ["exp", "iss", "sub", "aud"],
                "verify_exp": True,
                "verify_iss": True,
                "verify_aud": True,
            }
        )

        # token_typeの確認
        if payload.get("token_type") != "access":
            raise HTTPException(
                status_code=401,
                detail="無効なトークンタイプです"
            )

        return TokenPayload(payload)

    except jwt.ExpiredSignatureError:
        logger.warning(f"期限切れトークン: jti={_safe_get_jti(token)}")
        raise HTTPException(
            status_code=401,
            detail="トークンの有効期限が切れています",
            headers={"WWW-Authenticate": "Bearer error=\"invalid_token\""}
        )
    except jwt.InvalidIssuerError:
        raise HTTPException(status_code=401, detail="無効な発行者です")
    except jwt.InvalidAudienceError:
        raise HTTPException(status_code=401, detail="無効な対象者です")
    except jwt.InvalidSignatureError:
        logger.error("署名検証失敗 - 不正なトークンの可能性")
        raise HTTPException(status_code=401, detail="署名が無効です")
    except jwt.DecodeError:
        raise HTTPException(status_code=401, detail="トークンのデコードに失敗しました")


def _safe_get_jti(token: str) -> str:
    """安全にjtiを取得する(ログ用)"""
    try:
        unverified = jwt.decode(token, options={"verify_signature": False})
        return unverified.get("jti", "unknown")
    except Exception:
        return "decode_error"


# ============================================================
# RBAC(ロールベースアクセス制御)デコレータ
# ============================================================

def require_roles(*required_roles: str):
    """指定されたロールのいずれかを持つことを要求するデコレータ"""
    async def role_checker(
        current_user: TokenPayload = Depends(get_current_user)
    ) -> TokenPayload:
        if not current_user.has_any_role(list(required_roles)):
            logger.warning(
                f"権限不足: user={current_user.user_id}, "
                f"required={required_roles}, "
                f"actual={current_user.roles}"
            )
            raise HTTPException(
                status_code=403,
                detail=f"必要なロールがありません: {', '.join(required_roles)}"
            )
        return current_user
    return role_checker


def require_permissions(*required_permissions: str):
    """指定されたパーミッションを全て持つことを要求するデコレータ"""
    async def permission_checker(
        current_user: TokenPayload = Depends(get_current_user)
    ) -> TokenPayload:
        missing = [
            p for p in required_permissions
            if not current_user.has_permission(p)
        ]
        if missing:
            raise HTTPException(
                status_code=403,
                detail=f"必要な権限がありません: {', '.join(missing)}"
            )
        return current_user
    return permission_checker


# ============================================================
# APIエンドポイント
# ============================================================

@app.get("/api/v1/profile")
async def get_profile(
    current_user: TokenPayload = Depends(get_current_user)
):
    """認証済みユーザーのプロフィール取得(全ロール可)"""
    return {
        "user_id": current_user.user_id,
        "roles": current_user.roles,
        "token_expires": current_user.exp
    }


@app.get("/api/v1/logs")
async def get_logs(
    current_user: TokenPayload = Depends(require_roles("sre", "admin"))
):
    """ログ取得(SREまたは管理者のみ)"""
    return {
        "logs": [
            {"timestamp": "2024-01-15T10:00:00Z", "level": "INFO", "message": "Service started"},
            {"timestamp": "2024-01-15T10:01:00Z", "level": "ERROR", "message": "Connection timeout"}
        ]
    }


@app.post("/api/v1/config")
async def update_config(
    config: dict,
    current_user: TokenPayload = Depends(require_roles("admin"))
):
    """設定更新(管理者のみ)"""
    return {
        "status": "updated",
        "updated_by": current_user.user_id,
        "config": config
    }


@app.delete("/api/v1/users/{user_id}")
async def delete_user(
    user_id: str,
    current_user: TokenPayload = Depends(
        require_permissions("admin:users", "admin:delete")
    )
):
    """ユーザー削除(特定パーミッション必要)"""
    return {
        "status": "deleted",
        "deleted_user": user_id,
        "deleted_by": current_user.user_id
    }


# ============================================================
# ヘルスチェック(認証不要)
# ============================================================

@app.get("/health")
async def health_check():
    return {"status": "healthy", "timestamp": time.time()}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

8.3 JWKS Endpoint と PyJWKClient

#!/usr/bin/env python3
"""
JWKS Endpointを使用したJWT検証
PyJWKClientによる自動鍵取得・キャッシュ
"""

import jwt
from jwt import PyJWKClient
import time
import json
import logging
from typing import Optional
from functools import lru_cache

logger = logging.getLogger(__name__)


# ============================================================
# 1. JWKS Endpointからの鍵取得
# ============================================================

class JWKSVerifier:
    """JWKS Endpointを使用したJWT検証"""

    def __init__(
        self,
        jwks_uri: str,
        issuer: str,
        audience: str,
        algorithms: list[str] = None,
        cache_ttl: int = 300  # 5分
    ):
        self.jwks_uri = jwks_uri
        self.issuer = issuer
        self.audience = audience
        self.algorithms = algorithms or ["RS256"]

        # PyJWKClientはJWKSのキャッシュと自動更新を管理
        self.jwks_client = PyJWKClient(
            uri=jwks_uri,
            cache_keys=True,
            lifespan=cache_ttl,  # キャッシュTTL(秒)
            headers={
                "User-Agent": "MyApp/1.0 JWT-Verifier"
            }
        )

    def verify(self, token: str) -> dict:
        """JWTを検証する"""
        try:
            # Step 1: JWTヘッダーからkidを取得し、対応する公開鍵をJWKSから取得
            signing_key = self.jwks_client.get_signing_key_from_jwt(token)

            # Step 2: 取得した公開鍵でJWTを検証
            payload = jwt.decode(
                token,
                signing_key.key,
                algorithms=self.algorithms,
                issuer=self.issuer,
                audience=self.audience,
                options={
                    "require": ["exp", "iss", "sub", "aud"],
                    "verify_exp": True,
                    "verify_iss": True,
                    "verify_aud": True,
                }
            )

            logger.info(
                f"JWT検証成功: sub={payload['sub']}, "
                f"kid={signing_key.key_id}"
            )

            return payload

        except jwt.exceptions.PyJWKClientError as e:
            logger.error(f"JWKS鍵取得エラー: {e}")
            raise ValueError(f"署名鍵の取得に失敗しました: {e}")
        except jwt.ExpiredSignatureError:
            raise ValueError("トークンの有効期限が切れています")
        except jwt.InvalidSignatureError:
            raise ValueError("署名が無効です")
        except jwt.InvalidTokenError as e:
            raise ValueError(f"トークンが無効です: {e}")


# ============================================================
# 2. JWKS Endpointの実装例(FastAPI)
# ============================================================

from fastapi import FastAPI
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64

app = FastAPI()

# 鍵管理(本番環境ではVaultやKMS等を使用)
KEY_STORE = {}


def _int_to_base64url(value: int) -> str:
    """整数をBase64URL文字列に変換"""
    value_hex = format(value, 'x')
    if len(value_hex) % 2:
        value_hex = '0' + value_hex
    value_bytes = bytes.fromhex(value_hex)
    return base64.urlsafe_b64encode(value_bytes).rstrip(b'=').decode('utf-8')


def generate_and_store_key(kid: str):
    """RSA鍵を生成してストアに保存"""
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    KEY_STORE[kid] = private_key
    return private_key


@app.get("/.well-known/jwks.json")
async def jwks_endpoint():
    """
    JWKS Endpoint
    公開鍵セットをJSON形式で返す
    """
    keys = []
    for kid, private_key in KEY_STORE.items():
        public_key = private_key.public_key()
        public_numbers = public_key.public_numbers()

        jwk = {
            "kty": "RSA",
            "kid": kid,
            "use": "sig",
            "alg": "RS256",
            "n": _int_to_base64url(public_numbers.n),
            "e": _int_to_base64url(public_numbers.e),
        }
        keys.append(jwk)

    return {"keys": keys}


@app.get("/.well-known/openid-configuration")
async def openid_configuration():
    """
    OpenID Connect Discovery Endpoint
    """
    return {
        "issuer": "https://auth.example.com",
        "authorization_endpoint": "https://auth.example.com/authorize",
        "token_endpoint": "https://auth.example.com/token",
        "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
        "response_types_supported": ["code", "token", "id_token"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["RS256", "ES256"],
        "scopes_supported": ["openid", "profile", "email"],
        "token_endpoint_auth_methods_supported": [
            "client_secret_basic",
            "client_secret_post"
        ],
        "claims_supported": [
            "iss", "sub", "aud", "exp", "iat",
            "name", "email", "roles"
        ]
    }


# ============================================================
# 3. 使用例
# ============================================================

def demo_jwks_verification():
    """JWKSを使用した検証のデモ"""

    # JWKS検証者の初期化
    verifier = JWKSVerifier(
        jwks_uri="https://auth.example.com/.well-known/jwks.json",
        issuer="https://auth.example.com",
        audience="https://api.example.com"
    )

    # トークンの検証
    token = "eyJhbG..."  # 実際のトークン
    try:
        payload = verifier.verify(token)
        print(f"検証成功: {payload}")
    except ValueError as e:
        print(f"検証失敗: {e}")


# ============================================================
# 4. 鍵ローテーション対応
# ============================================================

class KeyRotationManager:
    """鍵ローテーション管理"""

    def __init__(self):
        self.current_kid: Optional[str] = None
        self.previous_kid: Optional[str] = None

    def rotate_keys(self) -> str:
        """鍵をローテーションする"""
        import datetime

        # 新しいkidを生成
        new_kid = f"key-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"

        # 現在の鍵を前の鍵に移動
        if self.current_kid:
            self.previous_kid = self.current_kid

        # 新しい鍵を生成
        generate_and_store_key(new_kid)
        self.current_kid = new_kid

        # 2世代前の鍵を削除
        keys_to_remove = [
            kid for kid in KEY_STORE
            if kid != self.current_kid and kid != self.previous_kid
        ]
        for kid in keys_to_remove:
            del KEY_STORE[kid]

        logger.info(
            f"鍵ローテーション完了: "
            f"current={self.current_kid}, "
            f"previous={self.previous_kid}, "
            f"removed={keys_to_remove}"
        )

        return new_kid

9. JWE(JSON Web Encryption)

9.1 JWEとは

JWE(JSON Web Encryption, RFC 7516)は、JWTのペイロードを暗号化するための仕様である。JWS(署名付きJWT)がデータの改ざん検知を提供するのに対し、JWEはデータの機密性を提供する。

9.2 JWEの構造

JWEはドット(.)で区切られた5つのパートで構成される。

JWEの構造:

[ヘッダー].[暗号化鍵].[初期化ベクトル].[暗号文].[認証タグ]
(Header) .(Encrypted .(IV)          .(Ciphertext).(Authentication
           Key)                                     Tag)

eyJhbGci...  .  YjKj8l...  .  48V1_A...  .  5eym8T...  .  XFBol...
     │              │             │             │            │
     │              │             │             │            │
     ▼              ▼             ▼             ▼            ▼
  JOSE          CEK を受信者    暗号化の      暗号化された   完全性検証用
  ヘッダー       の公開鍵で      ランダム      ペイロード     の認証タグ
  (Base64URL)   暗号化した値    値(IV)
                (Base64URL)    (Base64URL)   (Base64URL)   (Base64URL)

9.3 JWEのヘッダーフィールド

{
    "alg": "RSA-OAEP-256",     // 鍵暗号化アルゴリズム
    "enc": "A256GCM",          // コンテンツ暗号化アルゴリズム
    "typ": "JWT",              // トークンタイプ
    "kid": "encryption-key-1"  // 暗号化鍵の識別子
}
フィールド説明
alg鍵暗号化アルゴリズム(CEKの暗号化)RSA-OAEP, RSA-OAEP-256, A256KW, dir
encコンテンツ暗号化アルゴリズムA128CBC-HS256, A256CBC-HS512, A128GCM, A256GCM
typトークンタイプJWT
kid鍵識別子encryption-key-1
zip圧縮アルゴリズムDEF (DEFLATE)

9.4 JWE暗号化プロセス

【JWE暗号化プロセス】

  Step 1: CEK(Content Encryption Key)の生成
  ┌─────────────────────────────────────┐
  │ CEK = ランダム生成(256 bits等)     │
  └─────────────────────────────────────┘
           │
           ▼
  Step 2: CEKを受信者の公開鍵で暗号化
  ┌─────────────────────────────────────┐
  │ Encrypted_CEK = RSA-OAEP-256(       │
  │     CEK, recipient_public_key       │
  │ )                                   │
  └─────────────────────────────────────┘
           │
           ▼
  Step 3: IV(初期化ベクトル)の生成
  ┌─────────────────────────────────────┐
  │ IV = ランダム生成(96 bits for GCM) │
  └─────────────────────────────────────┘
           │
           ▼
  Step 4: ペイロードの暗号化
  ┌─────────────────────────────────────┐
  │ AAD = Base64URL(Header)             │
  │ (Ciphertext, Tag) = AES-256-GCM(   │
  │     plaintext = payload,            │
  │     key = CEK,                      │
  │     iv = IV,                        │
  │     aad = AAD                       │
  │ )                                   │
  └─────────────────────────────────────┘
           │
           ▼
  Step 5: JWEの組み立て
  ┌─────────────────────────────────────┐
  │ JWE = Base64URL(Header) + "." +     │
  │       Base64URL(Encrypted_CEK) + "."│
  │       Base64URL(IV) + "." +         │
  │       Base64URL(Ciphertext) + "." + │
  │       Base64URL(Tag)                │
  └─────────────────────────────────────┘

9.5 JWS vs JWE 比較

【JWS vs JWE の構造比較】

  JWS (署名付き):
  ┌──────────┬──────────┬──────────┐
  │  Header  │ Payload  │Signature │   3パート
  │(Base64URL)│(Base64URL)│(Base64URL)│
  └──────────┴──────────┴──────────┘
  ペイロードは平文(Base64URLデコードで読める)

  JWE (暗号化):
  ┌──────────┬──────────┬─────┬──────────┬──────────┐
  │  Header  │Encrypted │ IV  │Ciphertext│  Auth    │  5パート
  │          │   Key    │     │          │  Tag     │
  │(Base64URL)│(Base64URL)│(B64)│(Base64URL)│(Base64URL)│
  └──────────┴──────────┴─────┴──────────┴──────────┘
  ペイロードは暗号化されており、秘密鍵なしでは読めない
項目JWS(署名)JWE(暗号化)
目的改ざん検知・真正性の保証データの機密性の保護
パート数3パート5パート
ペイロードBase64URLエンコード(誰でも読める)暗号化(鍵なしでは読めない)
改ざん検知あり(署名で検証)あり(認証タグで検証)
機密性なしあり
サイズ比較的小さいJWSより大きい
処理速度速い遅い(暗号化/復号のオーバーヘッド)
ユースケースAPI認証トークン機密情報を含むトークン

9.6 ネストJWT(JWSをJWEで包む)

最も安全なアプローチは、まずJWSで署名し、その後JWEで暗号化する「ネストJWT」である。

【ネストJWT】

  Step 1: JWSの作成(内側)
  ┌─────────────────────────────────────────┐
  │ JWS = JWT.sign(payload, private_key)    │
  │                                         │
  │ eyJhbGc...eyJzdWI...SflKxw...          │
  └─────────────────────────────────────────┘
                    │
                    ▼
  Step 2: JWSをJWEで暗号化(外側)
  ┌─────────────────────────────────────────┐
  │ Header: {"alg":"RSA-OAEP-256",          │
  │          "enc":"A256GCM",               │
  │          "cty":"JWT"}  ← ネストを示す    │
  │                                         │
  │ JWE = JWT.encrypt(JWS, recipient_pubkey)│
  └─────────────────────────────────────────┘
                    │
                    ▼
  結果: 署名 + 暗号化の両方を持つトークン
  - 改ざん検知(JWSの署名)
  - 機密性保護(JWEの暗号化)

9.7 JWE実装例(Python)

#!/usr/bin/env python3
"""
JWEの実装例
必要なパッケージ: pip install PyJWT python-jose[cryptography]
"""

from jose import jwe, jwt as jose_jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import json
import time

# RSA鍵ペア生成(暗号化用)
enc_private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)

enc_private_pem = enc_private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

enc_public_pem = enc_private_key.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)


# JWEトークンの作成
def create_jwe_token(payload: dict, public_key: bytes) -> str:
    """JWEトークンを作成する"""
    plaintext = json.dumps(payload).encode('utf-8')

    token = jwe.encrypt(
        plaintext=plaintext,
        key=public_key,
        encryption="A256GCM",
        algorithm="RSA-OAEP-256",
        cty="JWT"
    )

    return token.decode('utf-8')


# JWEトークンの復号
def decrypt_jwe_token(token: str, private_key: bytes) -> dict:
    """JWEトークンを復号する"""
    plaintext = jwe.decrypt(
        token.encode('utf-8'),
        key=private_key
    )

    return json.loads(plaintext)


# 使用例
payload = {
    "sub": "user-123",
    "name": "山田太郎",
    "ssn": "123-45-6789",  # 機密情報(JWSでは平文で見える)
    "exp": int(time.time()) + 3600
}

# 暗号化
jwe_token = create_jwe_token(payload, enc_public_pem)
print(f"JWE Token: {jwe_token[:80]}...")
print(f"パート数: {len(jwe_token.split('.'))}")  # 5パート

# 復号
decrypted = decrypt_jwe_token(jwe_token, enc_private_pem)
print(f"復号結果: {decrypted}")

10. Istio/Envoy JWT検証

10.1 概要

Istioサービスメッシュでは、Envoyプロキシを使用してJWT検証をアプリケーションコードから分離できる。これにより、各マイクロサービスはJWT検証ロジックを実装する必要がなくなる。

【Istio JWT検証アーキテクチャ】

                        ┌──────────────────────────────────┐
                        │          Kubernetes Cluster       │
                        │                                  │
  Client                │  ┌─────────────────────────┐     │
    │                   │  │      Istio Ingress       │     │
    │  Authorization:   │  │      Gateway             │     │
    │  Bearer <JWT>     │  │  ┌─────────────────┐     │     │
    │──────────────────>│──│──│  Envoy Proxy    │     │     │
    │                   │  │  │  (JWT検証)       │     │     │
    │                   │  │  └────────┬────────┘     │     │
    │                   │  └───────────│──────────────┘     │
    │                   │              │                    │
    │                   │              ▼                    │
    │                   │  ┌─────────────────────────┐     │
    │                   │  │      Service A Pod       │     │
    │                   │  │  ┌─────────────────┐     │     │
    │                   │  │  │  Envoy Sidecar  │     │     │
    │                   │  │  │  (JWT検証)       │     │     │
    │                   │  │  └────────┬────────┘     │     │
    │                   │  │           │              │     │
    │                   │  │           ▼              │     │
    │                   │  │  ┌─────────────────┐    │     │
    │                   │  │  │  Application    │    │     │
    │                   │  │  │  (JWT検証不要)   │    │     │
    │                   │  │  │  ヘッダーから     │    │     │
    │                   │  │  │  ユーザー情報取得 │    │     │
    │                   │  │  └─────────────────┘    │     │
    │                   │  └─────────────────────────┘     │
    │                   │                                  │
    │                   │       ┌────────────────┐         │
    │                   │       │  JWKS Endpoint │         │
    │                   │       │  (鍵取得)       │         │
    │                   │       └────────────────┘         │
    │                   └──────────────────────────────────┘

10.2 RequestAuthentication

RequestAuthenticationリソースは、JWT検証のルールを定義する。

# request-authentication.yaml
# JWTの検証ルールを定義
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: jwt-auth
  namespace: production
spec:
  # 適用対象のワークロード
  selector:
    matchLabels:
      app: my-api
  jwtRules:
    # プライマリ認証プロバイダー
    - issuer: "https://auth.example.com"
      # JWKS URIから公開鍵を自動取得
      jwksUri: "https://auth.example.com/.well-known/jwks.json"
      # JWTの取得元(デフォルト: Authorization: Bearer ヘッダー)
      fromHeaders:
        - name: Authorization
          prefix: "Bearer "
      # または、クエリパラメータから取得
      # fromParams:
      #   - "access_token"
      # または、Cookieから取得
      # fromCookies:
      #   - "jwt_token"
      # Audience検証
      audiences:
        - "https://api.example.com"
        - "https://admin.example.com"
      # 転送ヘッダー設定
      # 検証済みペイロードをアプリケーションに転送
      outputPayloadToHeader: "x-jwt-payload"
      # クレームをヘッダーにマッピング
      outputClaimToHeaders:
        - header: "x-user-id"
          claim: "sub"
        - header: "x-user-roles"
          claim: "roles"
        - header: "x-user-email"
          claim: "email"

    # セカンダリ認証プロバイダー(複数のIdP対応)
    - issuer: "https://accounts.google.com"
      jwksUri: "https://www.googleapis.com/oauth2/v3/certs"
      audiences:
        - "my-app-client-id.apps.googleusercontent.com"

---
# 複数サービスへの適用(ワイルドカード)
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: jwt-auth-global
  namespace: istio-system  # mesh全体に適用
spec:
  # selector省略 = mesh内の全ワークロードに適用
  jwtRules:
    - issuer: "https://auth.example.com"
      jwksUri: "https://auth.example.com/.well-known/jwks.json"

10.3 AuthorizationPolicy

AuthorizationPolicyリソースは、JWTクレームに基づくアクセス制御を定義する。

# authorization-policy.yaml
# 認証されていないリクエストを拒否
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: require-jwt
  namespace: production
spec:
  selector:
    matchLabels:
      app: my-api
  action: DENY
  rules:
    # JWTが無い、または無効なリクエストを拒否
    - from:
        - source:
            notRequestPrincipals: ["*"]

---
# ロールベースアクセス制御
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: admin-only
  namespace: production
spec:
  selector:
    matchLabels:
      app: admin-dashboard
  action: ALLOW
  rules:
    - from:
        - source:
            # issuer/subject形式のprincipal
            requestPrincipals:
              - "https://auth.example.com/*"
      when:
        # JWTクレームに基づく条件
        - key: request.auth.claims[roles]
          values: ["admin"]

---
# パスベースのアクセス制御
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: api-access-control
  namespace: production
spec:
  selector:
    matchLabels:
      app: my-api
  action: ALLOW
  rules:
    # ヘルスチェックは認証不要
    - to:
        - operation:
            paths: ["/health", "/ready", "/metrics"]
            methods: ["GET"]

    # 一般APIは認証済みユーザーのみ
    - from:
        - source:
            requestPrincipals: ["*"]
      to:
        - operation:
            paths: ["/api/v1/*"]
            methods: ["GET"]

    # 書き込み操作はadminまたはsreロールのみ
    - from:
        - source:
            requestPrincipals: ["*"]
      to:
        - operation:
            paths: ["/api/v1/*"]
            methods: ["POST", "PUT", "DELETE"]
      when:
        - key: request.auth.claims[roles]
          values: ["admin", "sre"]

    # 特定のテナントのみアクセス可能
    - from:
        - source:
            requestPrincipals: ["*"]
      to:
        - operation:
            paths: ["/api/v1/tenant/*"]
      when:
        - key: request.auth.claims[tenant_id]
          values: ["org-456"]

---
# カスタムDENYポリシー(特定のクレーム値を拒否)
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: deny-expired-subscription
  namespace: production
spec:
  selector:
    matchLabels:
      app: my-api
  action: DENY
  rules:
    - when:
        - key: request.auth.claims[subscription]
          values: ["expired", "suspended"]

10.4 Envoy Filter(高度な設定)

# envoy-filter-jwt.yaml
# Envoyレベルでの詳細なJWT設定
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: jwt-advanced-config
  namespace: production
spec:
  workloadSelector:
    labels:
      app: my-api
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: SIDECAR_INBOUND
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
              subFilter:
                name: "envoy.filters.http.jwt_authn"
      patch:
        operation: MERGE
        value:
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
            providers:
              auth_provider:
                issuer: "https://auth.example.com"
                audiences:
                  - "https://api.example.com"
                remote_jwks:
                  http_uri:
                    uri: "https://auth.example.com/.well-known/jwks.json"
                    cluster: outbound|443||auth.example.com
                    timeout: 5s
                  cache_duration:
                    seconds: 300  # JWKSキャッシュ: 5分
                forward: true  # JWTをバックエンドに転送
                forward_payload_header: "x-jwt-payload"
                # クロック スキュー許容(秒)
                clock_skew_seconds: 30

10.5 完全なIstio設定例

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    istio-injection: enabled  # サイドカー自動注入

---
# gateway.yaml
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: api-gateway
  namespace: production
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      tls:
        mode: SIMPLE
        credentialName: api-tls-cert
      hosts:
        - "api.example.com"

---
# virtual-service.yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: api-routes
  namespace: production
spec:
  hosts:
    - "api.example.com"
  gateways:
    - api-gateway
  http:
    - match:
        - uri:
            prefix: /api/v1
      route:
        - destination:
            host: my-api
            port:
              number: 8080

---
# peer-authentication.yaml
# mTLS設定
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT  # サービス間通信はmTLS必須

---
# request-authentication.yaml
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
  name: jwt-auth
  namespace: production
spec:
  selector:
    matchLabels:
      app: my-api
  jwtRules:
    - issuer: "https://auth.example.com"
      jwksUri: "https://auth.example.com/.well-known/jwks.json"
      audiences:
        - "https://api.example.com"
      outputClaimToHeaders:
        - header: "x-user-id"
          claim: "sub"
        - header: "x-user-roles"
          claim: "roles"

---
# authorization-policy-deny-unauthenticated.yaml
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: deny-unauthenticated
  namespace: production
spec:
  selector:
    matchLabels:
      app: my-api
  action: ALLOW
  rules:
    # ヘルスチェック(認証不要)
    - to:
        - operation:
            paths: ["/health", "/ready"]
    # その他は認証必須
    - from:
        - source:
            requestPrincipals: ["*"]

---
# authorization-policy-rbac.yaml
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: rbac-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: my-api
  action: DENY
  rules:
    # admin APIはadminロール以外を拒否
    - to:
        - operation:
            paths: ["/api/v1/admin/*"]
      when:
        - key: request.auth.claims[roles]
          notValues: ["admin"]

10.6 トラブルシューティング

# JWT検証エラーの確認
kubectl logs -l app=my-api -c istio-proxy -n production | grep "jwt"

# Envoyの設定ダンプ
istioctl proxy-config listeners <pod-name> -n production -o json

# JWT認証ポリシーの確認
kubectl get requestauthentication -n production -o yaml
kubectl get authorizationpolicy -n production -o yaml

# テスト用リクエスト
# JWT無しでリクエスト → 403 Forbidden
curl -v https://api.example.com/api/v1/resource

# 有効なJWTでリクエスト → 200 OK
curl -v -H "Authorization: Bearer <valid-jwt>" \
  https://api.example.com/api/v1/resource

# 期限切れJWTでリクエスト → 401 Unauthorized
curl -v -H "Authorization: Bearer <expired-jwt>" \
  https://api.example.com/api/v1/resource

# Istioの分析ツール
istioctl analyze -n production

11. セキュリティベストプラクティス

11.1 アルゴリズム関連

許可アルゴリズムの明示的指定

# 悪い例: アルゴリズムを指定しない
decoded = jwt.decode(token, key)  # 危険!

# 良い例: 許可するアルゴリズムを明示的に指定
decoded = jwt.decode(
    token,
    key,
    algorithms=["RS256"]  # 許可アルゴリズムのホワイトリスト
)

noneアルゴリズムの無効化

# ライブラリレベルで"none"を拒否
ALLOWED_ALGORITHMS = ["RS256", "ES256"]  # "none"を含めない

decoded = jwt.decode(
    token,
    key,
    algorithms=ALLOWED_ALGORITHMS
)

11.2 鍵管理

【鍵管理のベストプラクティス】

  ┌──────────────────────────────────────────────────────┐
  │                     鍵管理                            │
  ├──────────────────────────────────────────────────────┤
  │                                                      │
  │  1. 鍵の強度                                          │
  │     - HMAC: 256 bits以上の乱数                        │
  │     - RSA: 2048 bits以上(推奨: 4096 bits)           │
  │     - ECDSA: P-256以上                               │
  │     - EdDSA: Ed25519                                 │
  │                                                      │
  │  2. 鍵の保管                                          │
  │     ┌─────────────┐                                  │
  │     │ 推奨         │                                  │
  │     │ - HSM        │ Hardware Security Module         │
  │     │ - KMS        │ Key Management Service           │
  │     │ - Vault      │ HashiCorp Vault等                │
  │     └─────────────┘                                  │
  │     ┌─────────────┐                                  │
  │     │ 避けるべき    │                                  │
  │     │ - ソースコード │ ハードコーディング                  │
  │     │ - 環境変数    │ プロセス一覧で見える可能性           │
  │     │ - 設定ファイル │ バージョン管理に含まれる可能性       │
  │     └─────────────┘                                  │
  │                                                      │
  │  3. 鍵のローテーション                                  │
  │     - 定期的なローテーション(90日推奨)                  │
  │     - 移行期間中は新旧両方の鍵で検証可能にする            │
  │     - JWKS Endpointで複数鍵を公開                      │
  │                                                      │
  │  4. 署名鍵と暗号化鍵の分離                              │
  │     - 署名用と暗号化用で別々の鍵ペアを使用               │
  │     - JWKの"use"フィールドで用途を明示                   │
  │                                                      │
  └──────────────────────────────────────────────────────┘
# Kubernetes Secretsからの鍵取得(例)
import os
import base64

# 鍵はKubernetes Secretsに保存
# apiVersion: v1
# kind: Secret
# metadata:
#   name: jwt-signing-keys
# data:
#   private-key: <base64-encoded-pem>
#   public-key: <base64-encoded-pem>

private_key_path = "/var/run/secrets/jwt/private-key"
public_key_path = "/var/run/secrets/jwt/public-key"

with open(private_key_path, 'rb') as f:
    private_key = f.read()

with open(public_key_path, 'rb') as f:
    public_key = f.read()

11.3 有効期限

トークンタイプ推奨有効期限理由
アクセストークン5-15分漏洩時の影響を最小化
リフレッシュトークン1-7日ユーザビリティとセキュリティのバランス
IDトークン5-60分認証後のセッション確立用
サービス間トークン5-15分マシン間通信用
from datetime import timedelta

# 推奨設定
ACCESS_TOKEN_EXPIRE = timedelta(minutes=15)
REFRESH_TOKEN_EXPIRE = timedelta(days=7)

# クロックスキュー(時刻のずれ)を考慮
CLOCK_SKEW_SECONDS = 30  # 30秒のずれを許容

decoded = jwt.decode(
    token,
    key,
    algorithms=["RS256"],
    leeway=CLOCK_SKEW_SECONDS  # クロックスキュー許容
)

11.4 クレーム検証

# 全ての必須クレームを検証する
decoded = jwt.decode(
    token,
    key,
    algorithms=["RS256"],
    issuer="https://auth.example.com",     # iss検証
    audience="https://api.example.com",     # aud検証
    options={
        "require": ["exp", "iss", "sub", "aud", "iat"],  # 必須クレーム
        "verify_exp": True,   # 有効期限検証
        "verify_nbf": True,   # 有効開始時刻検証
        "verify_iss": True,   # 発行者検証
        "verify_aud": True,   # 対象者検証
        "verify_iat": True,   # 発行時刻検証
    }
)

11.5 ペイロード

  • 機密情報を含めない: JWTのペイロードはBase64URLデコードすれば誰でも読める
  • サイズを最小限に: 大きなJWTはネットワークオーバーヘッドになる
  • 不必要な個人情報を含めない: GDPR等のプライバシー規制に注意
# 悪い例: 不必要な情報を含む
bad_payload = {
    "sub": "user-123",
    "name": "山田太郎",
    "email": "taro@example.com",
    "password_hash": "...",       # 絶対に含めない!
    "credit_card": "4111...",     # 絶対に含めない!
    "full_address": "...",        # 不必要な個人情報
    "social_security": "...",     # 絶対に含めない!
    "roles": ["admin"],
}

# 良い例: 最小限の情報のみ
good_payload = {
    "sub": "user-123",
    "roles": ["admin"],
    "iss": "https://auth.example.com",
    "aud": "https://api.example.com",
    "exp": 1716242622,
    "iat": 1716239022,
    "jti": "uuid-456"
}
# 詳細なユーザー情報はAPIで別途取得

11.6 トークン無効化(Revocation)

JWTは自己完結型であるため、発行後の無効化が本質的に難しい。以下の戦略がある。

【トークン無効化戦略】

  戦略1: 短い有効期限
  ┌─────────────────────────────────────────────┐
  │ アクセストークンの有効期限を短くする(5-15分)    │
  │ → 漏洩しても影響は短時間                       │
  │ → リフレッシュトークンで新しいトークンを取得      │
  └─────────────────────────────────────────────┘

  戦略2: トークンブラックリスト
  ┌─────────────────────────────────────────────┐
  │ Redis等に無効化されたjtiを保存                  │
  │                                              │
  │  JWT受信 → jtiをブラックリストで確認             │
  │           → ブラックリストにあれば拒否            │
  │                                              │
  │  注意: ステートフルになるため、JWTの利点が一部失われる│
  └─────────────────────────────────────────────┘

  戦略3: トークンバージョニング
  ┌─────────────────────────────────────────────┐
  │ ユーザーごとにトークンバージョンをDBに保存        │
  │                                              │
  │  JWT: {"sub": "user-123", "token_ver": 5}    │
  │  DB:  user-123 → current_version: 5          │
  │                                              │
  │  パスワード変更時: current_version を 6 に更新   │
  │  → token_ver: 5 のトークンは全て無効            │
  └─────────────────────────────────────────────┘
import redis

# ブラックリスト実装例
class TokenBlacklist:
    def __init__(self, redis_url: str):
        self.redis = redis.from_url(redis_url)

    def revoke(self, jti: str, exp: int):
        """トークンをブラックリストに追加"""
        import time
        ttl = exp - int(time.time())
        if ttl > 0:
            # 有効期限までブラックリストに保持
            self.redis.setex(f"blacklist:{jti}", ttl, "revoked")

    def is_revoked(self, jti: str) -> bool:
        """トークンがブラックリストにあるか確認"""
        return self.redis.exists(f"blacklist:{jti}") > 0

# 使用例
blacklist = TokenBlacklist("redis://localhost:6379")

# ログアウト時にトークンを無効化
blacklist.revoke(jti="uuid-456", exp=1716242622)

# 検証時にブラックリストを確認
if blacklist.is_revoked(payload["jti"]):
    raise ValueError("トークンは無効化されています")

11.7 JWKS関連

  • JWKS Endpointのキャッシュ: JWKS取得のレイテンシを軽減するためキャッシュする(5-15分)
  • フォールバック: JWKS Endpointがダウンした場合のフォールバック機構を用意する
  • 鍵ローテーション時の移行期間: 新旧両方の鍵をJWKSに含める

11.8 トランスポート

  • HTTPS必須: JWTは常にTLS/HTTPS経由で送信する
  • Strict Transport Security: HSTSヘッダーを設定する
  • Certificate Pinning: モバイルアプリではHTTPS証明書ピニングを検討する

11.9 ストレージ

保存場所セキュリティXSS耐性CSRF耐性推奨度
メモリ(変数)最も推奨(SPA)
HttpOnly Cookie低(要対策)推奨(SSR)
localStorage非推奨
sessionStorage非推奨
# HttpOnly Cookieでの安全な設定
Set-Cookie: access_token=<JWT>;
  HttpOnly;           # JavaScriptからアクセス不可
  Secure;             # HTTPS時のみ送信
  SameSite=Strict;    # CSRF対策
  Path=/api;          # APIパスのみ
  Max-Age=900;        # 15分
  Domain=.example.com

11.10 サイズ

JWTのサイズに注意する。HTTPヘッダーのサイズ制限(通常8-16KB)を超えないようにする。

import sys

# JWTサイズの確認
token = jwt.encode(payload, key, algorithm="RS256")
size = sys.getsizeof(token.encode('utf-8'))
print(f"JWTサイズ: {size} bytes")

# 目安:
# - 理想: 1KB未満
# - 許容: 2KB未満
# - 警告: 4KB以上
# - 問題: 8KB以上(一部のWebサーバーで拒否される可能性)

12. 一般的な脆弱性と攻撃

12.1 alg: none 攻撃

攻撃の概要

攻撃者がJWTヘッダーのalgnoneに変更し、署名なしのトークンを送信する攻撃。脆弱なライブラリはalg: noneを受け入れてしまう。

【alg:none 攻撃のフロー】

  正規のJWT:
  ┌────────────────────────────────────┐
  │ Header:  {"alg":"RS256","typ":"JWT"} │
  │ Payload: {"sub":"user-123","role":"user"} │
  │ Signature: <valid RSA signature>     │
  └────────────────────────────────────┘

  攻撃者が改ざん:
  ┌────────────────────────────────────┐
  │ Header:  {"alg":"none","typ":"JWT"}  │  ← algをnoneに変更
  │ Payload: {"sub":"user-123","role":"admin"} │  ← roleをadminに変更
  │ Signature: (空)                       │  ← 署名を削除
  └────────────────────────────────────┘

  脆弱なサーバー:
  alg=none → 署名検証をスキップ → 攻撃成功!

  安全なサーバー:
  alg=none → 許可アルゴリズムリストにない → 拒否!

対策

# 対策: 許可アルゴリズムを明示的にホワイトリスト化
ALLOWED_ALGORITHMS = ["RS256"]  # "none"は含めない

try:
    decoded = jwt.decode(
        token,
        key,
        algorithms=ALLOWED_ALGORITHMS  # 厳格なアルゴリズム指定
    )
except jwt.exceptions.InvalidAlgorithmError:
    # "none" や許可されていないアルゴリズムは拒否される
    raise ValueError("不正なアルゴリズムです")

12.2 アルゴリズム混同攻撃(Algorithm Confusion)

攻撃の概要

RS256(非対称鍵)で署名されたJWTを、HS256(対称鍵)として検証させる攻撃。攻撃者はRSA公開鍵(公開されている)をHMAC秘密鍵として使用してトークンを偽造する。

【アルゴリズム混同攻撃のフロー】

  正規のフロー:
  ┌─────────────────────────────────────────────┐
  │ サーバーの設定:                                │
  │   秘密鍵: RSA Private Key (秘密)             │
  │   公開鍵: RSA Public Key  (公開)             │
  │                                              │
  │ 署名: RSA-SHA256(data, RSA_Private_Key)      │
  │ 検証: RSA-SHA256-Verify(data, RSA_Public_Key)│
  └─────────────────────────────────────────────┘

  攻撃者のフロー:
  ┌─────────────────────────────────────────────┐
  │ 攻撃者が知っている情報:                        │
  │   RSA Public Key (公開されている)              │
  │                                              │
  │ 攻撃者の操作:                                 │
  │   1. ヘッダーのalgをRS256→HS256に変更          │
  │   2. RSA公開鍵をHMACの秘密鍵として使用         │
  │   3. HMAC-SHA256(data, RSA_Public_Key) で署名 │
  └─────────────────────────────────────────────┘

  脆弱なサーバー:
  ┌─────────────────────────────────────────────┐
  │ alg=HS256 を確認                              │
  │ → HMAC検証モードに切り替え                     │
  │ → 設定された「鍵」(= RSA Public Key) で検証    │
  │ → HMAC-SHA256(data, RSA_Public_Key) を計算    │
  │ → 攻撃者の署名と一致!                         │
  │ → 検証成功(攻撃成功!)                       │
  └─────────────────────────────────────────────┘

対策

# 対策1: アルゴリズムと鍵タイプの組み合わせを検証
def verify_token(token: str, rsa_public_key: bytes) -> dict:
    """安全なJWT検証"""
    # ヘッダーを事前に確認
    header = jwt.get_unverified_header(token)

    if header.get('alg') not in ['RS256']:
        raise ValueError(f"不許可のアルゴリズム: {header.get('alg')}")

    return jwt.decode(
        token,
        rsa_public_key,
        algorithms=["RS256"]  # RS256のみ許可
    )

# 対策2: 鍵タイプの自動判別を無効化
# PyJWTの最新版では、鍵タイプとアルゴリズムの不一致を自動的に拒否

12.3 鍵インジェクション攻撃(Key Injection / JWK Header Injection)

攻撃の概要

攻撃者がJWTヘッダーのjwkフィールドに自分の公開鍵を埋め込み、その対応する秘密鍵で署名する攻撃。脆弱なサーバーはヘッダー内の公開鍵を信頼して検証してしまう。

【鍵インジェクション攻撃のフロー】

  攻撃者が作成する悪意のあるJWT:
  ┌─────────────────────────────────────────────┐
  │ Header:                                      │
  │ {                                            │
  │   "alg": "RS256",                            │
  │   "typ": "JWT",                              │
  │   "jwk": {         ← 攻撃者の公開鍵を埋め込む  │
  │     "kty": "RSA",                            │
  │     "n": "攻撃者の公開鍵のモジュラス",           │
  │     "e": "AQAB"                              │
  │   }                                          │
  │ }                                            │
  │                                              │
  │ Payload: {"sub":"user-123","role":"admin"}    │
  │                                              │
  │ Signature: 攻撃者の秘密鍵で署名               │
  └─────────────────────────────────────────────┘

  脆弱なサーバー:
  ヘッダーのjwkフィールドから公開鍵を取得
  → 攻撃者の公開鍵で検証
  → 攻撃者の秘密鍵で署名されているので検証成功!

対策

# 対策: ヘッダー内の鍵を信頼しない
# 必ず事前に登録された鍵、またはJWKS Endpointの鍵を使用する

# 悪い例: ヘッダーから鍵を取得(脆弱!)
header = jwt.get_unverified_header(token)
key = header.get('jwk')  # 危険!

# 良い例: JWKS Endpointから鍵を取得
jwks_client = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
signing_key = jwks_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(token, signing_key.key, algorithms=["RS256"])

12.4 リプレイ攻撃(Replay Attack)

攻撃の概要

攻撃者が正規のJWTを傍受し、そのトークンを再利用する攻撃。

【リプレイ攻撃のフロー】

  正規ユーザー          攻撃者              サーバー
       │                 │                   │
       │  リクエスト       │                   │
       │  + JWT          │                   │
       │─────────────────│──────────────────>│
       │                 │                   │
       │                 │ JWTを傍受          │
       │                 │ (中間者攻撃等)     │
       │                 │                   │
       │  レスポンス      │                   │
       │<────────────────│───────────────────│
       │                 │                   │
       │                 │ 傍受したJWTで      │
       │                 │ リクエスト         │
       │                 │──────────────────>│
       │                 │                   │
       │                 │ 成功!(JWTは有効)  │
       │                 │<──────────────────│

対策

import time
import uuid

# 対策1: jtiによる一回限りの使用
class ReplayProtection:
    def __init__(self, redis_client):
        self.redis = redis_client

    def issue_token(self, payload: dict) -> str:
        jti = str(uuid.uuid4())
        payload["jti"] = jti
        return jwt.encode(payload, key, algorithm="RS256")

    def verify_token(self, token: str) -> dict:
        payload = jwt.decode(token, key, algorithms=["RS256"])
        jti = payload.get("jti")

        if not jti:
            raise ValueError("jtiクレームがありません")

        # jtiが既に使用済みかチェック
        if self.redis.exists(f"used_jti:{jti}"):
            raise ValueError("トークンは既に使用済みです(リプレイ攻撃の可能性)")

        # jtiを使用済みとして記録(expまでの期間のみ保持)
        ttl = payload["exp"] - int(time.time())
        if ttl > 0:
            self.redis.setex(f"used_jti:{jti}", ttl, "used")

        return payload

# 対策2: 短い有効期限 + HTTPS必須
# 対策3: nonceの使用
# 対策4: クライアントIPアドレスのバインディング

12.5 攻撃対策サマリー

┌─────────────────────┬────────────────────────────────────────┐
│     攻撃             │     対策                               │
├─────────────────────┼────────────────────────────────────────┤
│ alg:none            │ - アルゴリズムのホワイトリスト            │
│                     │ - "none"を明示的に拒否                  │
│                     │ - 最新のライブラリを使用                 │
├─────────────────────┼────────────────────────────────────────┤
│ アルゴリズム混同      │ - アルゴリズムと鍵タイプの検証            │
│                     │ - 非対称鍵使用時はRS/ES/PSのみ許可       │
│                     │ - 鍵とアルゴリズムのバインディング         │
├─────────────────────┼────────────────────────────────────────┤
│ 鍵インジェクション    │ - ヘッダー内の鍵を信頼しない              │
│                     │ - JWKS Endpointから鍵を取得              │
│                     │ - 鍵の事前登録・ピニング                 │
├─────────────────────┼────────────────────────────────────────┤
│ リプレイ             │ - jtiによる一回限り使用                  │
│                     │ - 短い有効期限(5-15分)                 │
│                     │ - HTTPS必須                             │
│                     │ - nonce/タイムスタンプ検証               │
├─────────────────────┼────────────────────────────────────────┤
│ トークン窃取         │ - HttpOnly Cookie使用                   │
│                     │ - XSS対策(CSP等)                      │
│                     │ - 短い有効期限                           │
│                     │ - Token Binding                        │
├─────────────────────┼────────────────────────────────────────┤
│ ブルートフォース      │ - 十分な鍵長の使用                       │
│                     │ - 非対称鍵アルゴリズムの優先              │
│                     │ - 鍵の定期ローテーション                  │
└─────────────────────┴────────────────────────────────────────┘

まとめ

JWTは現代のWeb・マイクロサービスアーキテクチャにおいて不可欠な認証・認可メカニズムである。本ガイドで解説した以下の要点を押さえることで、安全かつ効率的なJWT実装が可能になる。

  1. 構造の理解: Header.Payload.Signatureの3パート構造と各フィールドの役割
  2. 適切なアルゴリズム選定: ユースケースに応じたアルゴリズムの選択(RS256/ES256推奨)
  3. クレーム設計: 必要最小限の情報のみをペイロードに含める
  4. 鍵管理: HSM/KMS/Vaultによる安全な鍵管理と定期ローテーション
  5. 有効期限: アクセストークンは短寿命(15分以内)、リフレッシュトークンで更新
  6. JWKS活用: 公開鍵の配布と自動ローテーションにJWKS Endpointを活用
  7. Istio連携: サービスメッシュレベルでのJWT検証によりアプリケーションコードの簡素化
  8. セキュリティ対策: alg:none攻撃、アルゴリズム混同、鍵インジェクション等の既知の脆弱性への対策
  9. JWE活用: 機密情報を含む場合はJWEによる暗号化を検討

SREエンジニアとして、JWTの仕組みを深く理解し、適切な設定と監視を行うことで、安全で信頼性の高いシステムを構築・運用することができる。