DevToolBox

JWTの有効期限切れを確認する方法 - exp/iat/nbfの読み方と実例

APIを叩いたら突然 401 Unauthorized403 Forbidden が返ってくる。 ローカルでは動いていたのに本番だけ失敗する。こうした現象の多くはJWTトークンの有効期限切れが原因です。 この記事では、JWTの exp / iat / nbf クレームの意味と、 実際のトークンで失効を確認する手順、よくある落とし穴をまとめます。

図1: JWTの3つの部分

Header
eyJhbGciOi...
alg, typ
.
Payload
eyJzdWIiOi...
sub, exp, iat, nbf
.
Signature
SflKxwRJ...
HMAC / RSA

各パートはBase64URLエンコードされ、ドット(.)で連結される

TL;DR: 3つの時刻クレームだけ押さえれば十分

いずれもUTCのUnix秒(秒単位の整数)で格納されます。 ミリ秒ではなく秒である点を忘れると、new Date(exp)と書いて1970年になる罠にハマります。

図2: iat / nbf / exp の時間関係

iat発行nbf有効化now現在時刻exp失効有効期間時間軸 →

now が nbf と exp の間にあればトークンは有効。exp < now なら失効

失効しているかを手早く確認する手順

手順1: JWTをデコードする

JWTは header.payload.signature の3つをドットで繋いだ文字列で、それぞれBase64URLで エンコードされています。Payloadを取り出して中身を見れば、exp の値が分かります。

ブラウザで完結させたいなら、当サイトのJWT Decoderにトークンを貼り付けてください。ヘッダー・ペイロード・有効期限の判定までワンクリックで表示します (署名検証は行いません。デバッグ目的の表示専用です)。

手順2: exp を現在時刻と比較する

例として exp = 1714000000 というトークンがあったとします。 これを人間の時刻に直すには以下のように計算します。

// JavaScript での確認例
const exp = 1714000000;
const expiryDate = new Date(exp * 1000); // 秒 → ミリ秒
console.log(expiryDate.toISOString());
// -> "2024-04-24T22:26:40.000Z"

const now = Math.floor(Date.now() / 1000);
console.log(exp < now ? "expired" : "valid");

手順3: タイムゾーンの誤認を除外する

expは常にUTC基準です。サーバーがJSTで動いていても、比較する現在時刻もUTCで取得する必要があります。 JavaScriptなら Date.now()、Pythonなら time.time() が常にUTCを返すので、 そのまま秒に変換して比較すれば問題ありません。問題になるのはサーバーのOS時刻がずれているケースで、NTP同期が壊れていると数分の誤差で失効扱いになります。

よくある落とし穴

1. クロックスキュー(時計ずれ)

認証サーバーとAPIサーバーで時刻が数秒ズレているだけで、発行直後のトークンが 「まだ有効になっていない」扱いになります。多くのJWTライブラリはclockToleranceleeway オプションで許容秒数を指定できます。 本番環境では30秒程度の余裕を持たせるのが一般的です。

2. ミリ秒と秒の取り違え

Date.now() はミリ秒を返します。そのまま exp に入れると約3000万年先の時刻になり、 逆に exp * 1 のまま new Date に渡すと1970年になります。 必ず * 1000/ 1000 で単位を合わせてください。

3. リフレッシュトークンの仕組みを使っていない

アクセストークンの exp を長くして回避しようとする設計は、 盗難時のリスクが上がるため非推奨です。 短命のアクセストークン(15分〜1時間)+長命のリフレッシュトークンの組み合わせが現代の標準です。

4. ブラウザのlocalStorageに平文で保存している

XSSで一発で抜かれます。HttpOnly Cookieに格納するか、メモリ上のみに保持して リロード時に再取得する設計にするのが安全です。

401を受けたときの対処フロー

図3: 401/403受信時の切り分けフロー

1
JWT Decoder で exp を確認
2
失効 → リフレッシュトークンで再取得 / 有効 → 次へ
3
aud / iss が呼び出し先APIの期待値と一致するか
4
署名アルゴリズム (alg) の不一致をチェック
5
ローカル/サーバーの時刻ズレ (NTP) を確認
  1. JWT Decoderで exp を確認。失効していればリフレッシュ処理を試す
  2. 失効していないのに401なら、aud(想定する聴衆)と iss(発行元)が 呼び出し先APIの期待値と一致しているか確認
  3. 署名アルゴリズムの不一致(例: alg=none を誤って使っている)もよくある原因
  4. ローカルのシステム時刻が大幅にズレていないかも念のため確認

注意: 本番トークンを公共ツールに貼らない

JWTは署名されていても中身は平文で読めます。本番のトークンを扱う場合は、 ブラウザ内で完結するツール(本サイトのJWT Decoderなど)を使い、 サーバー送信型のツールへの貼り付けは避けてください。

関連ツール