DevToolBox

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

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

When an API suddenly returns 401 or 403, the fastest first check is JWT expiry. This guide explains the three time claims (exp, iat,nbf), shows how to read them as Unix epoch seconds, walks through timezone pitfalls, clock-skew tolerance, and the right recovery flow when a token has expired (rotate via refresh token rather than extending exp).

図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など)を使い、 サーバー送信型のツールへの貼り付けは避けてください。

English summary

JWT time claims are stored as Unix epoch seconds in UTC. exp = expiration,iat = issued-at, nbf = not-before. Compare againstDate.now() / 1000 (not milliseconds) and allow 30–60 seconds of clock skew. For a 401, always check exp first, then aud/iss. Never extend expto "fix" a stale token — rotate via refresh token. If JWT is decoded client-side for display, stay in browser-only tools like this site's JWT Decoder; do not paste tokens into server-hosted decoders.

関連ツール / Related tools