JWTの exp / iat / nbf — 実務で引っかかる時刻検証の落とし穴
JWT(JSON Web Token)の時刻クレーム — exp(expiration time)、iat(issued at)、nbf(not before) — は、RFC 7519 で定義されるシンプルな仕様です。しかし実務では、サーバ間の時刻ずれや、iatを信頼してしまう設計ミスなど、微妙な罠が複数あります。本記事では、JWTを発行・検証する側の実装者が踏みがちな落とし穴を5つ紹介します。
まずはクレームのおさらい
時刻クレームはすべてUNIX時間(UTC秒)で表現されます。ミリ秒ではありません。
| クレーム | 正式名 | 意味 |
|---|---|---|
exp | Expiration Time | この時刻以降、トークンは無効 |
iat | Issued At | このトークンが発行された時刻 |
nbf | Not Before | この時刻より前はトークンを使用不可 |
auth_time | Authentication Time (OIDC) | エンドユーザーが認証された時刻 |
典型的なJSONペイロード:
{
"sub": "user_123",
"iat": 1713340800,
"nbf": 1713340800,
"exp": 1713344400
}
罠その1: サーバー間の時計ずれ(clock skew)で期限切れ扱い
「発行して即リクエストに乗せたのに、検証側で "token expired" と出る」— これはマイクロサービス環境で頻出する問題です。原因は発行サーバーと検証サーバーの時計が数秒ずれていること。
対策: leeway(猶予時間)を設定する
ほとんどのJWTライブラリには clock skew を吸収する leeway オプションがあります。実装例(jsonwebtoken/jose):
// Node.js (jsonwebtoken)
jwt.verify(token, secret, { clockTolerance: 30 }); // 30秒
// jose (isomorphic)
await jwtVerify(token, secret, { clockTolerance: "30s" });
推奨値は30〜60秒。これより長くすると、期限切れトークンの再利用時間が延びてセキュリティ的に好ましくありません。
根本対策としては、全サーバーに NTP(chrony等)を導入して時計を同期させることが必須です。AWS EC2 や GCE なら標準でNTPが有効になっているため、通常は気にしなくて済みます。
罠その2: iat をセッション有効期限の基準に使う
「iatから24時間以内は有効にしたい」という要件で、検証側で now - iat < 86400 という独自ロジックを書くケースがあります。これは2つの問題を生みます。
- exp を無視している — 発行側が意図的にexpを短くしても、検証側で独自期限を使うと効かない
- iat は発行者が自由に設定できる — 攻撃者が古いトークンの iat を書き換えるわけではないが、iat だけに依存する設計は RFC 7519 の意図から外れる
正しいパターン: セッション有効期限は発行時に exp に含める。iat はログやデバッグ、必要に応じた経過時間の参考にとどめる。
// 発行時
const iat = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{ sub: userId },
secret,
{ expiresIn: "24h" } // exp を自動計算
);
// 検証時 - exp だけ見ればOK
jwt.verify(token, secret); // 期限切れなら自動的に TokenExpiredError
罠その3: nbf を使わずに「予約有効化」を実装する
nbf(not before)は、トークンをあらかじめ発行しておいて、ある時刻以降から有効にしたいケースで使います。具体的には:
- 招待リンクの「公開日時」設定
- スケジュール配信(キャンペーンメール等)
- リリース前のβテスト参加トークン
これを nbf ではなく「検証時にアプリ側で 現在時刻 > 指定時刻 をチェック」という実装で組むと、JWTライブラリの自動検証を回避してしまいます。
// ベター
const token = jwt.sign(
{
sub: userId,
nbf: Math.floor(releaseDate.getTime() / 1000),
},
secret,
{ expiresIn: "7d" }
);
// 検証側で特別な処理不要 - nbf前なら NotBeforeError が自動発生
注意: clock skew があるため、nbf ちょうどにアクセスすると「まだ有効でない」と弾かれる場合があります。clockTolerance オプションを忘れずに。
罠その4: リフレッシュトークンの exp と iat を混同する
アクセストークンと別にリフレッシュトークンを発行する2トークン方式で、時刻クレーム設計のミスが起きやすい箇所です。
よくある設計ミス:
- リフレッシュトークンの
expを「最終リフレッシュ時から30日」と勘違いする - 実際は発行時から30日で固定(標準JWT仕様)
「最終操作から30日」という rolling session を実現したい場合は、リフレッシュのたびに新しいリフレッシュトークンを再発行する必要があります(refresh token rotation)。新トークンの exp を発行時点+30日にすることで、アクティブユーザーのセッションが延長される仕組みです。
さらに、旧リフレッシュトークンを即失効させることで、盗まれた旧トークンでの同時利用を検出できます(OAuth 2.0 RFC 6819推奨)。
罠その5: exp のみで署名アルゴリズムを確認しない
これは時刻クレーム直接の話ではありませんが、"トークンの期限だけを見て安心してしまう"ケースとして最後に挙げます。
有名な脆弱性として alg: none 攻撃(署名アルゴリズムを "none" に書き換えて署名検証をスキップさせる)、HS256/RS256 混同攻撃(RS256で発行されたトークンの公開鍵をHS256の共有鍵として扱わせる)があります。
対策:
- ライブラリの
algorithmsオプションで期待するアルゴリズムをホワイトリストで明示指定する noneアルゴリズムは絶対に許可しない
// jsonwebtoken
jwt.verify(token, publicKey, {
algorithms: ["RS256"], // これを忘れない
clockTolerance: 30,
});
推奨実装パターンまとめ
- 時刻クレームは発行側で完結させる - 検証側で独自の時刻ロジックを書かない
- clock skew 対策として leeway を 30〜60秒設定 - サーバ間で必ず発生する誤差を吸収
- アクセストークンは短命(5〜15分)、リフレッシュトークンで延長 - 盗難時のリスクを最小化
- algorithms オプションを明示指定 - alg: none / HS256/RS256 混同攻撃を防ぐ
- 鍵ローテーションは kid(key ID)クレームと JWKS で - 鍵を無停止で更新できる構成にしておく
JWTの時刻検証はライブラリに任せるのが最も安全です。自前で書きたくなったら一旦立ち止まって、要件がライブラリのオプションで満たせないか再確認してください。
参考文献・ソース
本記事は NanToo(ナンツー)運営の株式会社ヨネマスが編集・公開しています。 ツールの開発現場で得た知見をもとに、実務で役立つ内容を発信しています。