The three time claims
| Claim | Meaning | Rule |
|---|---|---|
| exp | Expiration time | Reject if now ≥ exp (RFC 7519 §4.1.4) |
| nbf | Not before | Reject if now < nbf (§4.1.5) |
| iat | Issued at | Informational; a far-future iat suggests clock problems (§4.1.6) |
All three are NumericDate values: the number of seconds since 1970-01-01T00:00:00Z, ignoring leap seconds. A 13-digit value such as 1790606400000 is milliseconds — a common bug that pushes the expiry tens of thousands of years into the future. The checker flags it.
Clock skew and leeway
Servers' clocks drift. Most libraries accept a small leeway (typically 30–60 seconds) so a token isn't rejected a moment before or after its boundaries on a machine whose clock is slightly off. Keep leeway small: it effectively extends every token's lifetime.
// jose
await jwtVerify(token, key, { algorithms: ["RS256"], clockTolerance: 30 });
# PyJWT
jwt.decode(token, key, algorithms=["RS256"], leeway=30)How long should a JWT live?
A bearer token can be replayed by anyone who obtains it until it expires, so short lifetimes limit the damage of a leak. Access tokens commonly live 5–60 minutes and are renewed with a refresh token. Long-lived tokens (days or weeks) should be a deliberate decision — the decoder's security checks warn about lifetimes over a week.
“Token expired” but it shouldn't be?
- Compare the verifying server's clock with the issuer's — a few minutes of drift causes intermittent failures.
- Check whether exp was written in milliseconds or as a string.
- Check that the client actually uses the refreshed token (compare two tokens with the JWT diff).
Creating test tokens with a specific lifetime? JWTEncoder's timestamp builder turns “1 hour from now” into the exact NumericDate.