「Cognito でログインしているから安全」——この一文には、実は大きな飛躍があります。Cognito がトークンを発行することと、あなたのバックエンドがそのトークンを正しく検証することは、まったく別の話だからです。
JWT は AWS 公式の言葉を借りれば「簡単にデコードでき、読め、そして改ざんできる」ものです。検証を怠れば、改ざんされたアクセストークンは権限昇格を、改ざんされた ID トークンはなりすましを招きます。署名を検証して初めて、トークンの中身(claims)を信用してよい——これが本記事の出発点です。
この記事は、Cognito が発行する JWT(RS256) をバックエンドで正しく検証する実装に焦点を当てます。題材として、私が構築した招待制 B2B SaaS(7業種+閲覧/管理ロール、経済産業大臣賞を受賞した木材流通DX)で、API Gateway のオーソライザーと合わせて全 221 エンドポイントを保護し、第三者ペネトレーションテストで認証欠落 0 件を実証した設計判断も交えます。
この記事のルール:仕様の出典は AWS Cognito 公式ドキュメント(2026年6月時点) です。claim 名・JWKS URL・検証手順はすべて公式に基づきます。仕様は改定され得るため、本番投入前に必ず公式の「Verifying JSON web tokens」で最新を確認してください。そして大原則:トークンは「署名を検証して初めて」信用できる。署名検証前の claims は、ユーザーが書き換え可能な「ただの入力」です。
0. メンタルモデル:JWT は「署名された主張」である
設計を始める前に、たった一つの正しいモデルを共有させてください。
JWT は「署名された主張(signed claims)」です。 トークンの中身(sub は誰、token_use は何、exp はいつまで)は、発行者が「こう主張している」というデータに過ぎません。その主張を信用してよいかどうかを決めるのが署名検証です。
Cognito の JWT は .(ドット)で区切られた3つのセクションで構成されます(公式)。
- Header:鍵ID
kidと署名アルゴリズムalg。Cognito はalgにRS256(RSA + SHA-256)を使う。 - Payload:claims 本体。ID トークンならユーザー属性と
iss・aud、アクセストークンなら scope・iss・client_idなど。 - Signature:header と payload から計算された RSA256 の署名。JWKS URI で公開される公開鍵で検証する。
公式は明確にこう述べています。「悪意あるユーザーはトークンを改ざんできるが、アプリが公開鍵を取得して署名を比較すれば、改ざんされた署名は一致しない」。だから——OIDC 認証由来の JWT を処理するアプリは、サインインのたびにこの検証操作を必ず実行しなければならない。
検証とは、具体的には次の5つを満たすことです。本記事はこの5本柱を一つずつ実装していきます。
| # | 検証する対象 | 何を確認するか | 怠ると |
|---|---|---|---|
| 1 | 署名(RS256 / JWKS の kid) | header の kid で公開鍵を選び、RS256 で署名を検証 | 改ざんされたトークンを受理(権限昇格・なりすまし) |
| 2 | 発行者 iss | 自分の User Pool の issuer と完全一致するか | 別 Pool・別テナントのトークンを受理 |
| 3 | 対象 aud / client_id | 自分の App Client ID と一致するか | 別アプリ向けのトークンを流用される |
| 4 | 有効期限 exp / iat | 期限切れでないか(clock skew を考慮) | 失効済みトークンを受理 |
| 5 | token_use | 受け取るべき種別(id or access)と一致するか | ID と Access の取り違え(後述) |
1. 二種類のトークン:ID トークンとアクセストークンは別物
「Cognito のトークン」と一括りにすると、ここで必ず詰まります。Cognito は認証成功時に ID トークン・アクセストークン・リフレッシュトークンの3つを返します(リフレッシュトークンは暗号化された不透明文字列で、検証の対象外)。検証対象になるのは ID トークンとアクセストークンの2つで、目的も claims も署名鍵も別です。
公式の記述を整理します。
| ID トークン | アクセストークン | |
|---|---|---|
| 目的(公式) | ユーザーの**身元(identity)**を表す。name・email・phone_number などの属性 claim | API 操作の認可。scope に基づくリソースアクセス |
token_use の値 | id | access |
| 対象を表す claim | aud(認証した App Client) | client_id(同じ値が ID トークンの aud に入る) |
| 主な claim | sub・email・cognito:groups・aud・iss・exp・iat・auth_time | sub・scope・cognito:groups・client_id・username・iss・exp・iat |
| 署名鍵 | ID トークン専用の鍵 | アクセストークン専用の別の鍵 |
ここで実装上きわめて重要な公式の注意点があります。
Cognito は User Pool ごとに RSA 鍵ペアを2組生成する。一方がアクセストークンを、もう一方が ID トークンを署名する。同じユーザーセッションでも、アクセストークンの
kidと ID トークンのkidは一致しない。 コード内では ID トークンとアクセストークンを独立して検証すること。
つまり「ID もアクセスも同じ検証器でまとめて通す」は誤りです。検証器はトークン種別ごとに分け、期待する token_use を固定します。
どちらを検証するべきか
| シナリオ | 検証すべきトークン | 理由 |
|---|---|---|
| OAuth 2.0 scope でリソース API を認可したい | アクセストークン | scope はアクセストークンにのみ入る。API 認可はアクセストークンの本来の目的(公式) |
| API Gateway の JWT オーソライザーや Cognito の self-service 操作 | アクセストークン | API Gateway はアクセストークンでの認可をサポート(公式) |
| 自前バックエンドでユーザー属性(email 等)を直接使いたい | ID トークン | 属性 claim は ID トークンに入る |
OAuth 2.0 の原則では「API 認可=アクセストークン」が王道です。一方で本記事の題材である木材流通 DX のプロジェクトは、ID トークンを採用し、token_use == id 以外を拒否しました。招待制で App Client が限定された閉じた B2B 環境であり、認可の判断にユーザー属性(業種・ロール)を直接使う設計だったため、ID トークンが要件に素直に合致したからです。
重要なのは「どちらが正解か」ではなく、「実際に使うトークン種別と token_use の検証を一致させる」ことです。アクセストークンを受け取るのに token_use == id を要求したら、正規のトークンが全部弾かれます。逆に、ID トークンを受け取る API で token_use を検証しなければ、scope を持たない ID トークンを「認可済み」と誤認しかねません。
2. JWKS:公開鍵をどこから取得するか
署名検証に必要な公開鍵は、User Pool の JWKS(JSON Web Key Set)エンドポイントから取得します。URL は公式で次の形式と定められています。
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
返ってくる JSON は、複数の公開鍵(JWK)の配列です。公式のサンプルを引用します。
{
"keys": [
{
"kid": "1234example=",
"alg": "RS256",
"kty": "RSA",
"e": "AQAB",
"n": "1234567890",
"use": "sig"
},
{
"kid": "5678example=",
"alg": "RS256",
"kty": "RSA",
"e": "AQAB",
"n": "987654321",
"use": "sig"
}
]
}
各フィールドの意味(公式):
kid:JWS の署名に使われた鍵を示すヒント。トークン header のkidと突き合わせる鍵。alg:RS256(RSA + SHA-256)。kty:鍵の種類。ここではRSA。e/n:RSA 公開鍵の指数(exponent)と法(modulus)。Base64urlUInt エンコード。use:鍵の用途。sig(署名検証用)。
鍵が複数あるのは、ID トークン用とアクセストークン用で別鍵だから(前章)であり、さらに鍵ローテーション中は新旧の鍵が並ぶためです。だから「最初の鍵を使う」ような実装は誤りで、必ず kid で正しい鍵を選ぶ必要があります。
3. 自前で検証する:Python での実装(5本柱を一つずつ)
ここからは「ライブラリに任せず、何が起きているかを理解する」ための実装です。本番では第6章の aws-jwt-verify を推奨しますが、仕組みを理解しないまま使うと落とし穴を踏むので、まず手で組み立てます。
依存は PyJWT(cryptography 付き)と requests を想定します。
3.1 JWKS の取得と kid による鍵選択
"""Cognito JWT を検証する最小実装(理解のため)。本番は aws-jwt-verify を推奨(第6章)。"""
from __future__ import annotations
import time
from dataclasses import dataclass
import jwt
import requests
from jwt.algorithms import RSAAlgorithm
@dataclass(frozen=True)
class CognitoConfig:
region: str
user_pool_id: str
app_client_id: str
expected_token_use: str # "id" または "access"。受け取る種別に合わせて固定する
@property
def issuer(self) -> str:
# iss はこの形式と完全一致しなければならない(公式)
return f"https://cognito-idp.{self.region}.amazonaws.com/{self.user_pool_id}"
@property
def jwks_url(self) -> str:
return f"{self.issuer}/.well-known/jwks.json"
def fetch_jwks(cfg: CognitoConfig) -> dict[str, dict]:
"""JWKS を取得し、kid をキーにした dict に変換する。"""
resp = requests.get(cfg.jwks_url, timeout=5)
resp.raise_for_status() # 取得失敗を握り潰さない(fail-closed)
return {key["kid"]: key for key in resp.json()["keys"]}
ポイントは raise_for_status() です。JWKS の取得に失敗したら検証を続行してはいけません(fail-closed)。後述する「JWKS 無検証取得」の落とし穴です。
3.2 kid マッチング → RS256 署名検証
公式の手順は明快です。「トークン header の kid と JWKS の kid を突き合わせ、一致する公開鍵で署名を検証する」。ここで alg をトークン任せにせず、RS256 に固定するのが最重要のセキュリティ対策です(理由は第7章の alg=none / アルゴリズム混同攻撃)。
def verify_token(token: str, cfg: CognitoConfig, jwks: dict[str, dict]) -> dict:
"""署名 → iss → aud/client_id → exp/iat → token_use の順で検証し、claims を返す。"""
# (1) header だけを先に読む。ここではまだ claims を信用しない
header = jwt.get_unverified_header(token)
kid = header.get("kid")
if kid is None or kid not in jwks:
# kid が未知 → 鍵ローテーションの可能性。呼び出し側で JWKS を再取得して再試行する
raise ValueError("unknown kid: JWKS の再取得が必要かもしれません")
# (2) kid に対応する公開鍵を JWK から構築
public_key = RSAAlgorithm.from_jwk(jwks[kid])
# (3) 署名検証+iss/exp/iat/aud をライブラリに検証させる
claims = jwt.decode(
token,
key=public_key,
algorithms=["RS256"], # ★ alg をホワイトリスト固定。トークンの alg を信用しない
issuer=cfg.issuer, # iss を検証(不一致は例外)
audience=cfg.app_client_id if cfg.expected_token_use == "id" else None,
leeway=60, # clock skew(時計のズレ)を 60 秒許容
options={
"require": ["exp", "iat", "iss"], # 必須 claim を強制
"verify_signature": True,
"verify_exp": True,
"verify_iat": True,
},
)
# (4) token_use を検証(PyJWT は知らない claim なので自前で)
if claims.get("token_use") != cfg.expected_token_use:
raise ValueError(
f"token_use mismatch: expected {cfg.expected_token_use}, got {claims.get('token_use')!r}"
)
# (5) アクセストークンは aud ではなく client_id を自前で照合する
if cfg.expected_token_use == "access":
if claims.get("client_id") != cfg.app_client_id:
raise ValueError("client_id mismatch")
return claims # ここまで通って初めて claims を信用してよい
PyJWT の jwt.decode に渡している項目が、そのまま公式の検証手順に対応しています。
algorithms=["RS256"]:最重要。トークン header のalgを信用せず、こちらが許可した RS256 のみ受理。issuer=cfg.issuer:公式が言う「issは User Pool と一致しなければならない」。audience=...:ID トークンはaud(PyJWT が検証可能)。アクセストークンはaudではなくclient_idなので、audienceを渡さず (5) で自前照合。leeway=60:clock skew。サーバー間の時計ズレで正規トークンを誤って弾かないための許容秒数。require=["exp", "iat", "iss"]:これらの claim が無いトークンを拒否(claim の欠落=検証スキップを防ぐ)。
そして公式の token_use 検証ルールを (4) で実装しています。
token_useclaim を確認する。アクセストークンのみ受理するならその値はaccessでなければならない。ID トークンのみ使うならその値はidでなければならない。両方使うならidかaccessのいずれかでなければならない。(公式)
設計の要点(SRP):
fetch_jwks(鍵の取得)とverify_token(検証)を分離しています。鍵取得は I/O・キャッシュの責務、検証は純粋な暗号・claim 照合の責務。混ぜると、テストもキャッシュ戦略も破綻します。
4. JWKS のキャッシュと定期更新:本番のシングルトン
第3章の fetch_jwks をリクエストごとに呼ぶのは論外です。JWKS エンドポイントへの外部 HTTP がボトルネックになり、レート制限や一時障害でログイン全体が落ちます(Single Point of Failure)。
一方、公式はこう釘を刺します。
Cognito は User Pool の署名鍵をローテーションすることがある。ベストプラクティスとして、公開鍵を
kidをキーにアプリ内にキャッシュし、定期的にキャッシュを更新せよ。受け取ったトークンのkidをキャッシュと比較し、正しい issuer なのに未知のkidを受け取ったら、鍵がローテーションされた可能性があるのでjwks_uriからキャッシュを更新せよ。
つまり要件は2つ:(a) キャッシュして外部 I/O を減らす、(b) ローテーションに追従する。木材流通 DX では、これを二重チェックロックのシングルトンでキャッシュし、**6時間間隔で更新(起動時にプリウォーム)**する形で実装しました。これを一般化したのが次のコードです。
"""JWKS を二重チェックロックでキャッシュし、定期更新+未知kidで即時リフレッシュする。"""
import threading
import time
class JwksCache:
"""スレッドセーフな JWKS シングルトンキャッシュ。
- 起動時プリウォームでコールドスタートの遅延を排除
- refresh_interval ごとに自動更新(鍵ローテーション追従)
- 未知 kid を引いたら即時リフレッシュ(公式の推奨どおり)
"""
def __init__(self, cfg: CognitoConfig, refresh_interval: float = 6 * 3600) -> None:
self._cfg = cfg
self._refresh_interval = refresh_interval
self._lock = threading.Lock()
self._jwks: dict[str, dict] = {}
self._fetched_at = 0.0
def prewarm(self) -> None:
"""起動時に呼ぶ。最初のリクエストで JWKS 取得の遅延を負わせない。"""
self._refresh()
def _is_stale(self) -> bool:
return (time.monotonic() - self._fetched_at) > self._refresh_interval
def _refresh(self) -> None:
jwks = fetch_jwks(self._cfg) # 取得失敗は例外で伝播(fail-closed)
self._jwks = jwks
self._fetched_at = time.monotonic()
def _ensure_fresh(self) -> None:
if not self._is_stale():
return
# 二重チェックロック:ロック取得は鮮度が切れているときだけ
with self._lock:
if self._is_stale(): # 待っている間に他スレッドが更新済みかを再確認
self._refresh()
def get_key(self, kid: str) -> dict:
self._ensure_fresh()
if kid in self._jwks:
return self._jwks[kid]
# 正しい issuer なのに未知 kid → ローテーションの可能性。一度だけ強制更新して再試行
with self._lock:
if kid not in self._jwks:
self._refresh()
if kid not in self._jwks:
raise ValueError(f"kid {kid!r} not found even after refresh")
return self._jwks[kid]
二重チェックロック(double-checked locking)の意図は明確です。鮮度が切れていない通常時はロックを取らず(高速・並行)、切れたときだけロックを取り、しかもロック取得後にもう一度鮮度を確認して、待っている間に他スレッドが更新済みなら二重取得を避けます。鍵ローテーション時の「未知 kid → 強制更新 → 再試行」も、公式の推奨フローそのものです。
コスト効率と信頼性の両立:6時間間隔は「ローテーション追従の十分さ」と「外部 I/O の少なさ」のバランス点です。未知 kid での即時更新があるので、間隔を短くしすぎてエンドポイントを叩き続ける必要はありません(YAGNI)。プリウォームは「最初の一人だけ遅い」を消すための投資です。
5. 検証をどこで行うか:API Gateway オーソライザー × アプリ内検証の二層
「トークン検証はどこでやるべきか」は設計判断です。選択肢を整理します。
| 検証する場所 | 長所 | 短所 | 向くケース |
|---|---|---|---|
| API Gateway オーソライザー(JWT/Lambda) | 各サービスに到達する前に弾ける。共通化・運用が楽。署名・iss・aud・exp を一括検証 | プラットフォーム依存。きめ細かい認可(業種・ロール)はやりにくい | 全 API の一次防衛線 |
| アプリ内検証 | scope・cognito:groups・業種ロールなどドメイン固有の認可まで踏み込める | 各サービスに実装が必要(DRY 違反になりがち) | 細粒度の認可が必要な層 |
| 両方(二層) | 一次防衛(GW)+ドメイン認可(アプリ)で多層防御 | 実装コスト | 本番の B2B SaaS |
木材流通 DX では両方を採用しました。API Gateway のオーソライザーで全 221 エンドポイントを一次防衛し(署名・iss・aud・exp・token_use を検証、token_use == id 以外を拒否)、業種ベースの認可はルーター層で frozenset のホワイトリスト照合により厳格化(不一致は 403、ID 列挙攻撃を抑止)しました。
この「二層」が効くのは、責務が違うからです。署名・発行者・有効期限の検証は「このトークンは本物か」という認証の問い。業種・ロールの照合は「この本物のユーザーはこの操作をしてよいか」という認可の問い。前者を GW で共通化し、後者をアプリのドメイン層に置くことで、認可ロジックの変更が GW 設定に波及しません(ETC:変更が局所化される)。
fail-open を fail-closed に:セキュリティ監査では、Webhook や JWT 周りの「エラー時に通してしまう(fail-open)」挙動を、すべて**エラー時は拒否する(fail-closed)**に是正しました。検証コードで例外を握り潰して
return Trueしてしまう実装は、最悪のバグです。迷ったら閉じる——これがセキュリティの既定値です。
6. 推奨実装:aws-jwt-verify に任せる
ここまで手で組み立てましたが、本番で推奨されるのは公式が名指しする aws-jwt-verify を使うことです。公式ドキュメントの記述を引用します。
Node.js アプリでは、AWS は
aws-jwt-verifyライブラリを推奨する。CognitoJwtVerifierに検証したい claim 値を設定できる。チェックできるのは、アクセス/ID トークンが壊れていない・期限切れでない・有効な署名を持つこと、正しい User Pool と App Client から来たこと、正しい OAuth 2.0 scope を含むこと、署名鍵のkidが JWKS URI の鍵と一致すること、など。
TypeScript(このサイトと同じスタック)での推奨実装は次のとおりです。検証器はハンドラの外で一度だけ作るのが要点で、これで JWKS キャッシュが再利用されます(第4章を自前で書く必要がなくなる)。
import { CognitoJwtVerifier } from "aws-jwt-verify";
// ★ ハンドラの外で一度だけ生成 → JWKS キャッシュが再利用される
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID!, // iss と JWKS URL を自動導出
tokenUse: "id", // token_use を固定("id" or "access")
clientId: process.env.COGNITO_APP_CLIENT_ID!, // id なら aud、access なら client_id を照合
});
export async function verify(token: string) {
try {
// 署名・kid・iss・aud/client_id・token_use・exp/nbf をまとめて検証
const payload = await verifier.verify(token);
return payload; // 検証済み claims
} catch {
// 失敗時は必ず拒否(fail-closed)。理由をログに残すなら PII は出さない
throw new Error("invalid token");
}
}
CognitoJwtVerifier.create の3つのパラメータが、本記事の5本柱に直結します(公式 README)。
userPoolId:issuer と JWKS エンドポイントを自動導出。iss検証と鍵取得を任せられる。tokenUse:token_useclaim が期待値("id"/"access")と一致するかを検証。clientId:ID トークンならaud、アクセストークンならclient_idを照合。
ライブラリは発行者ごとに JWKS をメモリキャッシュし、kid で鍵を選びます。JWKS は最初に一度だけ取得し、未知の kid(=鍵ローテーション)が現れたときだけ再取得します——第4章で手作りしたロジックが、そのまま標準で備わっています。
なぜ自前実装より推奨されるか(DRY / 信頼性):公式 README は「
issuerとaudienceのようなセキュリティ上重要な claim を明示的に指定させる設計にすることで、利用者が安全に使いやすくしている」と述べます。検証ロジックは「車輪の再発明」をすると落とし穴を踏みやすい領域です。第3章を理解した上で、本番は枯れたライブラリに寄せる——これが正解です。Python なら同等のpython-jose+自前検証や、トークン種別ごとの検証器を使います。
7. セキュリティの落とし穴(ここが本番の分かれ目)
検証コードは「書けば安全」ではありません。有名な抜け穴を一つでも踏むと、検証全体が無意味になります。
7.1 alg=none / アルゴリズム混同攻撃
最も古典的かつ致命的な攻撃です。攻撃者がトークン header の alg を none に書き換え、署名を空にして送る——ライブラリがトークンの alg を信用すると、署名検証をスキップして通してしまう。さらに alg を HS256(HMAC)に変えて、公開鍵(誰でも知れる)を HMAC の秘密鍵として使わせるアルゴリズム混同攻撃もあります。
対策は一つ:検証側でアルゴリズムをホワイトリスト固定すること。第3章の algorithms=["RS256"] がこれです。トークン header の alg は絶対に信用しない。Cognito は RS256 しか使わない(公式)のだから、RS256 以外を受理する理由はありません。
7.2 token_use を検証しない
第1章のとおり、ID とアクセスは目的も鍵も別です。token_use を検証しないと、例えば「scope を必要とする API」にアクセストークンの代わりに ID トークン(scope を持たない)が通ってしまう、あるいはその逆が起きます。公式が token_use の検証を独立した手順として明記しているのは、これが検証漏れの定番だからです。受け取る種別を token_use で固定してください。
7.3 aud / client_id を検証しない
iss(発行者)だけ見て aud / client_id を見ないと、同じ User Pool の別 App Client 向けに発行されたトークンを流用されます。マルチクライアント環境では致命的です。ID トークンは aud、アクセストークンは client_id——種別ごとに正しい claim を照合します(公式:両者は同じ値を指す)。
7.4 JWKS を無検証・無防備に取得する
JWKS の取得は HTTPS(証明書検証あり)で行い、**取得失敗時は検証を中止(fail-closed)**します。取得失敗を握り潰して「鍵が無いから検証スキップ」にすると、検証そのものが無効化されます。また、未知 kid を引くたびに無制限に再取得するとエンドポイントへの DoS を自分で誘発するので、再取得は「一度だけ・キャッシュ前提」に制限します(第4章)。
7.5 署名検証前に claims を使う
jwt.get_unverified_header で header を読むのは鍵選択のためだけ。payload の claims(sub・email・cognito:groups 等)を、署名検証が完了する前に使ってはいけません。 「ログ出力のために先に sub を読む」程度でも、未検証の値を業務ロジックや認可判断に流す入口になります。検証を通った返り値の claims のみを使う、という規律を徹底します。
7.6 失効・ローテーション
exp は「有効期限」であって「失効」ではありません。ユーザーを即時に締め出したいケース(退会・権限剥奪・漏洩)では、exp だけでは足りません。Cognito はトークン失効(revocation)に対応しており、リフレッシュトークンを revoke すると、同じ origin_jti を持つアクセス/ID トークンが無効化されます(公式)。即時失効が要件なら、origin_jti / jti を使った失効チェックや、短い exp(Cognito は 5分〜1日で設定可能)と組み合わせます。
7.7 clock skew(時計のズレ)
サーバー間の時計は必ず多少ズレます。exp / iat を厳密に秒単位で比較すると、正規のトークンを「期限切れ」「未来発行」として誤って弾く事故が起きます。第3章の leeway=60 のように、**数十秒〜数分の許容(leeway)**を入れます。ただし大きくしすぎると失効が遅れるので、最小限にとどめます。
8. 役割分担:この記事は「検証の実装」、設計・SSO は別記事
混乱を避けるため、関連記事との役割を明確にしておきます。
- 認証の設計(複雑な認可要件の組み立て方)は 複雑な認証要件のCognito設計 で扱います。グループ・ロール・テナント分離の設計の話です。
- エンタープライズ SSO(SAML / OIDC 連携)は SAML/OIDCエンタープライズSSO で扱います。外部 IdP とのフェデレーション設計の話です。
- 本記事は、それらで発行された **JWT をバックエンドが正しく検証する「実装とセキュリティ」**に特化しています。
設計・SSO はそちらへ、トークン検証の実装はこちらへ——重複なく補完し合う構成です。
9. まとめ:検証チートシート
迷ったときの早見表です。
- 5本柱を全部やる:① 署名(RS256・JWKS の
kid)②iss③aud/client_id④exp/iat(+ clock skew)⑤token_use。どれか一つでも欠けたら検証は不完全。 algはRS256にホワイトリスト固定:トークン header のalgを絶対に信用しない(alg=none・混同攻撃対策)。- トークン種別を取り違えない:受け取る種別と
token_useを一致させる。ID はaud、Access はclient_id。鍵も別なので独立して検証。 - JWKS はキャッシュ+定期更新+未知 kid で即時リフレッシュ:起動時プリウォーム、6時間間隔が一つの実用解。取得失敗は fail-closed。
- 検証はライブラリに寄せる:本番は
aws-jwt-verify(公式推奨)。検証器はハンドラ外で一度だけ生成し、JWKS キャッシュを再利用。 - どこで検証するか:API Gateway オーソライザーで一次防衛、ドメイン認可(業種・ロール・scope)はアプリ層へ。二層で多層防御。
- 迷ったら閉じる:例外・取得失敗・claim 欠落はすべて拒否(fail-closed)。
JWT 検証は「署名された主張を、署名を確かめてから信用する」という一行の原則に集約されます。けれど本番では、kid の取り違え、token_use の見落とし、alg の固定漏れ、JWKS のキャッシュ戦略、fail-open の罠——一つでも踏むと検証全体が崩れる繊細な領域です。
私は招待制 B2B SaaS(経済産業大臣賞を受賞した木材流通 DX)で、Cognito の RS256 + JWKS を基盤に、API Gateway オーソライザーと合わせて全 221 エンドポイントを保護し、token_use の固定・JWKS のシングルトンキャッシュ・fail-open の fail-closed 化までを設計・実装しました。その結果、第三者ペネトレーションテスト(実在 15 ロール)で全 221 エンドポイントの認証欠落 0 件を実証しています。これらを、一人 × 生成AI(Claude Code)で、速く・安全に作り切りました。
「自社の Cognito 認証、本当に正しく検証できていますか?」——トークン検証の設計レビューから API 全体の認可設計、ペネトレ指摘の是正まで、一気通貫で伴走できます。お気軽にご相談ください。
参考(公式ドキュメント)
- Verifying JSON web tokens(Amazon Cognito) — 検証手順・JWKS URL・
kid・token_useの正規ルール - Understanding user pool JSON web tokens (JWTs) — ID/アクセストークンの位置づけ・claims・refresh/revoke
- Understanding the identity (ID) token — ID トークンの claim 一覧(
token_use: id・aud・issほか) - Understanding the access token — アクセストークンの claim 一覧(
token_use: access・client_id・scopeほか) - aws-jwt-verify(GitHub・AWS Labs) — AWS 公式推奨の JWT 検証ライブラリ・
CognitoJwtVerifierAPI・JWKS キャッシュ