JWTの有効期限切れを確認する方法 - exp/iat/nbfの読み方と実例
APIを叩いたら突然 401 Unauthorized や 403 Forbidden が返ってくる。 ローカルでは動いていたのに本番だけ失敗する。こうした現象の多くはJWTトークンの有効期限切れが原因です。 この記事では、JWTの exp / iat / nbf クレームの意味と、 実際のトークンで失効を確認する手順、よくある落とし穴をまとめます。
図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など)を使い、 サーバー送信型のツールへの貼り付けは避けてください。