JWTの有効期限切れを確認する方法 - exp/iat/nbfの読み方と実例
APIを叩いたら突然 401 Unauthorized や 403 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つの部分
各パートはBase64URLエンコードされ、ドット(.)で連結される
TL;DR: 3つの時刻クレームだけ押さえれば十分
exp(Expiration Time): トークンが失効する時刻(Unix秒)iat(Issued At): トークンが発行された時刻nbf(Not Before): トークンが有効になる時刻。発行直後は使えない仕様のトークンで登場
いずれもUTCのUnix秒(秒単位の整数)で格納されます。 ミリ秒ではなく秒である点を忘れると、new Date(exp)と書いて1970年になる罠にハマります。
図2: iat / nbf / 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ライブラリはclockTolerance や leeway オプションで許容秒数を指定できます。 本番環境では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受信時の切り分けフロー
exp を確認aud / iss が呼び出し先APIの期待値と一致するかalg) の不一致をチェック- JWT Decoderで
expを確認。失効していればリフレッシュ処理を試す - 失効していないのに401なら、
aud(想定する聴衆)とiss(発行元)が 呼び出し先APIの期待値と一致しているか確認 - 署名アルゴリズムの不一致(例:
alg=noneを誤って使っている)もよくある原因 - ローカルのシステム時刻が大幅にズレていないかも念のため確認
注意: 本番トークンを公共ツールに貼らない
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.