【実務・中級編】 JWTのペイロードにおける標準クレーム(sub, iat, nbf, jti)の定義 – Web APIアーキテクチャ・データ連携実践ガイド

現場のエンジニア諸君、今日もパケットの海を泳いでいるか。

Web APIの設計において、認証の「鍵」となるJWT(JSON Web Token)。多くのエンジニアが「なんとなく」ライブラリに任せて実装しているが、RFC 7519の仕様を噛み砕き、その中身を制御できるかどうかで、システムの堅牢性は天と地ほどの差が出る。

今日は、JWTのペイロードにおける標準クレーム(Registered Claim Names)の中でも、特に「認証の整合性と安全性」に直結する sub, iat, nbf, jti に焦点を当てて深掘りしよう。

—

JWTのペイロード:ただの連想配列と思うなかれ

JWTのペイロードは、いわば「身分証明書の記載事項」だ。ここを適当に設計すると、リプレイ攻撃やトークンの乗っ取りという致命的な脆弱性を招く。

各クレームの役割を整理しておこう。

  • sub (Subject): 誰のためのトークンか。ユーザーIDやAPIの識別子が入る。
  • iat (Issued At): いつ発行されたか。不正利用の追跡に必須。
  • nbf (Not Before): いつから有効か。サーバー間の時刻同期ズレを考慮したバッファとして使うこともある。
  • jti (JWT ID): トークンの一意な識別子。これがリプレイ攻撃対策の「最後の砦」だ。

—

リプレイ攻撃を封じる:jti の実戦的運用

リプレイ攻撃とは、盗聴された有効なトークンを悪意のある第三者が再送してくる攻撃だ。サーバー側で exp(有効期限)をチェックしていても、期限内であればサーバーは「正しいリクエスト」として処理してしまう。

ここで jti の出番だ。「同じ jti を持つリクエストを二度受け取らない」というホワイトリスト(あるいはブラックリスト)の仕組みをRedis等の高速なKVSで構築することで、一度使われたトークンを確実に無効化できる。

Pythonによる検証ロジックのイメージ

jti を使ったリプレイガードのロジックは、サーバー側のミドルウェアで以下のように実装する。

import redis
import time

# Redis接続(インフラチームに感謝しつつ接続する)
r = redis.StrictRedis(host='localhost', port=6379, db=0)

def verify_jti(payload):
    jti = payload.get("jti")
    exp = payload.get("exp")
    
    # 1. jtiが存在するか確認
    if not jti:
        raise Exception("jti missing")
    
    # 2. Redisにjtiが存在するか(既に使われていないか)
    if r.exists(f"jti:{jti}"):
        raise Exception("Replay attack detected!")
    
    # 3. トークン有効期限までRedisに保存(自動削除時間を設定)
    ttl = int(exp - time.time())
    r.setex(f"jti:{jti}", ttl, "used")
    
    return True

—

現場で使えるデバッグTips:トークンの正体を見極める

開発中、APIが401 Unauthorizedを返したとき、まずはトークンの内容を疑うのがプロの鉄則だ。curl と jq を使って、パケットをキャプチャする前に中身を覗き見よう。

# トークンをデコードしてペイロードを確認するコマンド例
# ヘッダーとペイロードを分割してBase64デコードする
JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

echo $JWT_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .

もし nbf が未来の日付になっている場合、サーバーの時刻同期(NTP)がズレている可能性がある。ネットワークスペシャリストとしては、まず chrony や ntpd のステータスを疑うべきだ。

—

APIクライアント側の実装(Fetch API)

クライアント側では、標準クレームを意識したリクエストヘッダーの構築が必要だ。以下は、認証が必要なエンドポイントへのリクエスト例である。

const token = "取得したJWT";

fetch('https://api.example.com/v1/resource', {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
})
.then(response => {
  if (response.status === 401) {
    console.error("トークンの有効期限切れ、あるいは無効なjtiです。");
  }
  return response.json();
});

—

結び:プロトコルと心中する覚悟で

sub, iat, nbf, jti 。これらの一つ一つは単なる文字列だが、正しく運用すれば強固な防壁となり、疎かにすればシステムの脆弱性の入り口となる。

RFCは「ルールブック」ではなく、「先人たちが泥臭いトラブルと戦って導き出した最適解」だ。仕様を読み込み、なぜそのフィールドが必要なのか、パケットの裏側で何が起きているのかを想像しながら設計してほしい。

何かあれば、またこのネットワークの深淵で語り合おう。健闘を祈る。

コメント

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