DevToolBox

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

最終更新日: 2026-04-19公開日: 2026-04-19執筆: DevToolBox編集部

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エンコードされ、ドット(.)で連結される

JWT Decoder

トークンを貼り付けるだけで exp・iat を日本時間に変換して表示。期限切れかどうかをその場で判定できます。

今すぐ試す →

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-24T23:06: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に格納するか、メモリ上のみに保持して リロード時に再取得する設計にするのが安全です。

実際のJWTでexpを検証してみる

以下は secret your-256-bit-secret でHS256署名したサンプルJWTです。 Node.jsの標準モジュールcryptoだけで生成・署名検証済みで、外部ライブラリは使っていません。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRhcm8gWWFtYWRhIiwiaWF0IjoxNjk5OTk2NDAwLCJleHAiOjE3MDAwMDAwMDB9.hjkOZ1KjD-X6S1dl61SEuAdhVNAbvxKThTNaFB5L4zk

ペイロード部分(2番目のセグメント)だけをBase64URLデコードすると、次のJSONが得られます。

{
  "sub": "1234567890",
  "name": "Taro Yamada",
  "iat": 1699996400,
  "exp": 1700000000
}

exp の値 1700000000 を人間が読める日時に変換する方法は2通りあります。

方法1: JavaScriptで変換

new Date(1700000000 * 1000).toISOString();
// -> "2023-11-14T22:13:20.000Z"

方法2: コマンドラインで変換(Linux/macOS)

date -u -d @1700000000
# -> Tue Nov 14 22:13:20 UTC 2023

いずれも結果は 2023年11月14日 22:13:20 UTC。このexp値は本記事の執筆時点(2026年)から見ると すでに過去なので、このサンプルトークンは常に失効状態です。動作確認用として安心して使えます (秘密鍵が公開されているため、本番では絶対に使わないでください)。

当サイトのJWT Decoderにこの文字列をそのまま貼り付ければ、 上記と同じペイロードと失効判定がワンクリックで表示されることも確認できます。

ライブラリごとの期限切れエラーの違い

exp切れをどう検知するかはライブラリによって挙動が異なります。エラーハンドリングを書く前に、 使っているライブラリが何をどう投げるかを公式ドキュメント・ソースコードで確認しておくと事故を防げます。

ライブラリ言語exp切れ時の例外許容誤差オプション
jsonwebtokenNode.jsTokenExpiredErrorerr.expiredAtに失効時刻)clockTolerance(秒)
joseNode.js / ブラウザJWTExpiredcode: "ERR_JWT_EXPIRED"clockTolerance(秒 or 文字列)
PyJWTPythonExpiredSignatureErrorleeway(秒)

いずれも例外の型名で失効を判定できるため、catchブロックでerr instanceof TokenExpiredError のような分岐を書けば「失効だけリフレッシュ処理へ回す」 実装が素直に書けます。エラーメッセージの文字列一致で判定するのは変更に弱いため避けたほうが安全です。

クロックスキュー実務: leewayはなぜ必要か

認証サーバーとリソースサーバーが別ホストで動いていると、OS時刻がミリ秒〜数秒単位でズレることがあります。 これが原因で次のような「本来は正常なのにエラーになる」パターンが起きます。

この問題への対処が leeway / clockTolerance オプションです。 JWTの標準仕様であるRFC 7519のSection 4.1.4(expクレームの定義)には、 実装者は時刻比較の際に「通常数分を超えないわずかな余裕(small leeway)」を持たせてよいと明記されています。 実務では数十秒(30秒前後)を許容する設定が一般的です。これはNTP同期が機能していれば通常のサーバー間時刻ズレが 数十ミリ秒〜数秒程度に収まるためで、leewayを大きくしすぎるとexpの意味そのものが薄れる点に注意してください。

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 exp to "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.

よくある質問 / FAQ

JWTの有効期限はどこを見ればわかりますか?
ペイロードの exp クレームです。Unix秒で入っているので、現在時刻と比較すれば失効しているか判定できます。
exp・iat・nbf の違いは何ですか?
exp は失効する時刻、iat は発行された時刻、nbf はそれ以前は使えない開始時刻です。失効判定に使うのは exp です。
有効期限内なのに401が返るのはなぜですか?
サーバとクライアントの時刻ずれ(clock skew)、署名検証の失敗、audienceやissuerの不一致など、期限以外の原因が考えられます。
JWTのデコードに秘密鍵は必要ですか?
不要です。ヘッダーとペイロードはBase64URLで符号化されているだけなので誰でも中身を読めます。秘密鍵が必要なのは署名の検証だけです。そのためJWTに秘密情報を入れてはいけません。

関連ツール / Related tools