こんにちは、インフラアーキテクトの私です。
日々のAPI設計やクラウドネイティブなインフラ運用、お疲れ様です。認証基盤の設計で「とりあえずJWT(JSON Web Token)を使おう」と決めたものの、ふと立ち止まって「本当にこのペイロードの中身は安全なのか?」「改ざんされたらどうやって検知されるんだっけ?」と、RFCの仕様書(RFC 7519)を夜な夜なめくった経験はないでしょうか。
Web APIの設計において、ステートレスな認証のデファクトスタンダードとなったJWTですが、その内部構造と改ざん検知メカニズムを正確に理解しているエンジニアは、意外と少ないものです。
今回は、パケットの往来や暗号の裏側まで見通すネットワークスペシャリストの視点から、JWTの3つのセグメント(ヘッダー、ペイロード、署名)の正体に迫り、実務で絶対に外せない検証の仕組みをコードとシーケンスを交えて徹底解説します。
—
1. JWTの正体:なぜ「ドット(.)」で区切られた3つの文字列なのか?
まずは実物を見てみましょう。APIのレスポンスやAuthorizationヘッダーに含まれるJWTは、以下のような文字列です。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
なんだか暗号のように見えますが、実はこれ、「暗号化されているわけではありません」。ここが最大のポイントです。JWTの本体は単なるJSONであり、誰でもデコードして中身を読むことができます。
この文字列は、Header、Payload、Signatureという3つの要素がドット(.)で連結されているだけにすぎません。
[Header] . [Payload] . [Signature]
↑ ↑ ↑
アルゴリズム ユーザー情報 改ざん検知用の
メタデータ (クレーム) 暗号署名
これらはすべて、パディング文字の = を省き、URLセーフな文字に置換した Base64URL という方式でエンコードされています。つまり、中身は丸見えなのです。「じゃあセキュリティ的に危なくないか?」という疑問が湧きますよね。それを担保するのが、3つ目のセグメントである「署名(Signature)」です。
—
2. JWTの構造を分解する:3つのセグメントの役割
それぞれのセグメントがどのような役割を持っているのか、詳しく紐解いていきましょう。
① ヘッダー (Header)
ヘッダーには、トークン自体のメタデータ、具体的には「どの暗号化アルゴリズムを使っているか (alg)」や「トークンの種類 (typ)」が格納されます。
{
"alg": "HS256",
"typ": "JWT"
}
alg: 署名に利用されるアルゴリズム(例:HS256は HMAC-SHA256、RS256は RSA Signature with SHA-256)。typ: トークンのタイプ(通常はJWT)。
② ペイロード (Payload)
ペイロードには、ユーザーに関する情報や、トークンの有効期限などの「クレーム(Claims)」と呼ばれるデータが格納されます。
{
"sub": "1234567890",
"name": "Taro Network",
"admin": true,
"iat": 1710000000
}
sub(Subject): ユーザーを一意に識別するID。name: ユーザー名。admin: 独自クレーム(権限管理フラグなど)。iat(Issued At): トークンが発行された日時(UNIXエポック秒)。
繰り返しになりますが、このヘッダーとペイロードは、誰でもBase64URLデコードすれば一瞬で中身が読めます。そのため、パスワードや機密情報をペイロードに含めることは絶対にタブーです。
③ 署名 (Signature)
ここが肝心要です。署名は、サーバー側だけが知っている秘密鍵(あるいは公開鍵に対する秘密鍵)を使い、以下のような計算式で生成されます。
HMAC-SHA256(base64UrlEncode(Header) + "." + base64UrlEncode(Payload), SecretKey)
この署名があるおかげで、「誰かが途中でペイロードの admin: false を admin: true に書き換えた」としても、サーバー側で再計算した署名と一致しなくなるため、一発で改ざんを検知できるのです。
—
3. 改ざん検知のメカニズム:通信フローと検証プロセス
では、APIリクエストを受け取ったサーバー側で、JWTがどのように検証されているのか、その通信フローと内部処理を確認してみましょう。
シーケンス:クライアントからAPIサーバーへの検証フロー
Client (Web/Mobile) API Gateway / Backend
│ │
│─── 1. API Request + JWT ──────>│
│ (Header.Payload.Signature) │
│ │
│ ├─ 2. トークンを "." で分割
│ ├─ 3. Header と Payload を取り出す
│ ├─ 4. サーバ側の秘密鍵で署名を再計算
│ │ (CalcSig = HMAC(H.P, Secret))
│ │
│ ├─ 5. CalcSig = 届いた Signature?
│ │ ├─ 一致: 正常(処理続行)
│ │ └─ 不一致: 改ざん/不正検知(401)
│ │
│<── 300 OK / 401 Unauthorized ──┘
サーバーは受信したJWTを一度 .(ドット) で分解します。そして、送信されてきた Header と Payload を取り出し、あらかじめサーバーが安全に保管しているシークレットキー(秘密鍵)を使って、再度署名をハッシュ計算します。
もし、悪意あるユーザーがクライアント側でPayloadの数値を書き換えて送信していた場合、サーバー側で再計算した署名と、リクエストに含まれていた署名が一致しなくなります。サーバーはここで「このトークンは途中で改ざんされた、あるいは偽造されたものだ」と判断し、容赦なく 401 Unauthorized を返すわけです。
—
4. 実務で役立つ!Pythonによる署名検証と改ざん検知の実装例
百聞は一見にしかず。Pythonの定番ライブラリ PyJWT を使って、実際にJWTの生成から改ざん検知(検証失敗)までの挙動をコードで確認してみましょう。
事前の準備として、ライブラリをインストールしてください。
pip install pyjwt
以下のスクリプトを実行すると、正当なトークンの検証成功と、ペイロードが改ざんされた場合の例外発生(検知)をシミュレートできます。
import jwt
import time
# サーバー側のみが知る極秘の秘密鍵(実際は環境変数等で厳重に管理)
SECRET_KEY = "super-secret-infrastructure-key"
ALGORITHM = "HS256"
def create_jwt_token(user_id: str, is_admin: bool) -> str:
"""ユーザー情報からJWTを生成する(発行処理)"""
payload = {
"sub": user_id,
"admin": is_admin,
"iat": int(time.time()),
"exp": int(time.time()) + 3600 # 1時間後に有効期限切れ
}
# 署名を含んだJWT文字列を生成
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
return token
def verify_jwt_token(token: str):
"""JWTを検証し、ペイロードを取り出す(APIサーバー側の処理)"""
try:
# 署名の検証 および 有効期限(exp)のチェックを同時に行う
decoded_payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print("[成功] トークンは正常です。ペイロード:", decoded_payload)
return decoded_payload
except jwt.ExpiredSignatureError:
print("[エラー] トークンの有効期限が切れています (401)")
except jwt.InvalidSignatureError:
print("[警告・改ざん検知] 署名が一致しません!データが改ざんされた可能性があります (401)")
except jwt.PyJWTError as e:
print(f"[エラー] その他の検証エラー: {e}")
if __name__ == "__main__":
# 1. 通常のユーザーとしてトークンを生成
valid_token = create_jwt_token(user_id="user_999", is_admin=False)
print(f"生成されたJWT:\n{valid_token}\n")
# 2. 正常な検証テスト
print("--- 正常なリクエストの検証 ---")
verify_jwt_token(valid_token)
# 3. 悪意あるユーザーがクライアント側でトークンを勝手に改ざんするシミュレーション
# (例: ペイロード部分を書き換えて admin: true に仕立て上げる)
print("\n--- 改ざんされたリクエストの検証シミュレーション ---")
# 実際には文字列を直接いじるか、偽のペイロードで再エンコードする
# ここでは改ざんされた偽のトークンを手動で用意(または署名なしで作り直す等)
tampered_payload = {
"sub": "user_999",
"admin": True, # 権限を勝手に管理者へ昇格させた!
"iat": int(time.time()),
"exp": int(time.time()) + 3600
}
# 攻撃者は正しい秘密鍵を知らないため、適当な鍵で署名するか、署名を破壊する
tampered_token = jwt.encode(tampered_payload, "wrong-secret-key", algorithm=ALGORITHM)
# サーバー側で検証すると...
verify_jwt_token(tampered_token)
このコードを実行すると、攻撃者が秘密鍵を知らずに不正な署名(あるいは違う鍵)で生成したトークンに対し、サーバーが jwt.InvalidSignatureError を検知してブロックする様子がよく分かります。
—
5. 現場のインフラエンジニアがハマる「JWT運用」の落とし穴とTips
最後に、現場のトラブルシューティングや設計レビューで私たちが直面しがちな「よくある罠」をいくつかシェアしておきます。
① alg: none の脆弱性(アルゴリズム・フィックス攻撃)
初期の古いJWTライブラリでは、ヘッダーの alg に none(署名なし)を指定して渡すと、サーバー側が「署名の検証をスキップする」という致命的な脆弱性を抱えているものがありました。
- 対策: ライブラリは常に最新版を使用し、サーバー側で許可するアルゴリズムを厳格にホワイトリスト方式(例:
HS256やRS256のみ)でハードコーディングしておきましょう。
② 暗号化と署名の混同
先述した通り、標準的なJWT(JWS)は暗号化されていません。クレジットカード番号や個人の機密情報をBase64URLデコード可能なペイロードに載せるのは規約違反(GDPRや個人情報保護法の観点からもNG)です。機密情報を載せる必要がある場合は、暗号化レイヤーを含む JWE (JSON Web Encryption) の採用を検討してください。
③ 鍵の管理とローテーション
HMAC(HS256 など)は共通鍵暗号方式のため、複数のマイクロサービスすべてに同じシークレットキーを配布する必要があります。1つのサービスがコンプロマイズ(漏洩)した際の影響範囲が広すぎるため、本番環境のシステム規模が大きい場合は、公開鍵暗号方式である RSA (RS256) や ECDSA (ES256) を採用し、認証サーバー(IdP)だけが秘密鍵を持ち、APIサーバー群は公開鍵で検証のみを行うアーキテクチャにするのがベストプラクティスです。
—
まとめ
JWTは、ただの文字列の組み合わせに見えて、その裏側では堅牢な暗号数学(HMACやRSA)によってデータの完全性と改ざん検知が担保されています。
- ペイロードは誰でも読める(暗号化ではない)
- 信頼の根幹は「署名(Signature)」にある
- サーバー側での秘密鍵による再計算と検証が必須
この基本原則さえ押さえておけば、クラウドネイティブなAPI設計や、万が一のセキュリティインシデントにおけるフォレンジック調査でも迷うことはありません。
皆さんのWeb API設計が、より堅牢で美しいものになることを願っています。それでは、また次回の深淵なるプロトコル解説でお会いしましょう!
コメント