Error message
"exp" claim timestamp check failed
jose (JavaScript) — JWTExpired, code ERR_JWT_EXPIRED
What it means
jose's claim validation rejected the token because its exp timestamp is in the past relative to the verifier's clock. The signature check had already passed — this is purely a freshness failure. jose raises the same wording pattern for other claims ("nbf" claim timestamp check failed) when a token is used before its not-before time.
Why it happens
The token outlived its lifetime
commonAccess tokens are designed to be short-lived. A client caching tokens without checking expiry, a queue consumer processing a message hours after it was enqueued with a token inside, or a test fixture with a hard-coded token will all land here deterministically.
Verifier clock ahead of issuer clock
commonA few minutes of clock drift on the verifying host shortens every token's effective lifetime and can reject freshly issued tokens. Containers inherit their host clock — a drifting VM breaks all pods on it at once.
Wrong unit written into exp
occasionalexp must be seconds since epoch (RFC 7519). Issuing code that divides incorrectly or passes a Date object serialized as milliseconds produces tokens that are either expired at birth or valid for millennia.
How to fix it
- 1.
Add bounded clock tolerance
jose accepts a clockTolerance option (number of seconds or a human-readable string). Keep it small; it is a drift absorber, not a lifetime extension.
js import { jwtVerify } from "jose"; await jwtVerify(token, key, { algorithms: ["ES256"], clockTolerance: "30s", }); - 2.
Print the timeline before changing anything
Compare iat, exp and the verifier's now. The pattern tells you the cause: exp seconds in the past → refresh problem; exp before iat → issuing bug; exp ≈ now but failing → skew.
js import { decodeJwt } from "jose"; const { iat, exp } = decodeJwt(token); console.log({ iat: new Date(iat * 1000), exp: new Date(exp * 1000), now: new Date() });