ASVS 5.0 V9.2.1 says that when a token carries a validity time span, you accept the token only if the verification time falls inside it. For JWTs that means checking nbf (not before) and exp (expiry). It is a Level 1 requirement.
Why it matters
An expiry that nobody enforces is a token that lives forever. A leaked token from a log file, a screenshot or an old backup stays useful to the attacker for exactly as long as your verifier lets it.
How to implement it
Most libraries check exp and nbf by default when present, but two things go wrong: someone switches the check off, or the claim is simply missing and the token is accepted as never expiring. Require the claims.
await jwtVerify(token, key, {
algorithms: ["ES256"],
issuer: ISSUER,
audience: AUDIENCE,
requiredClaims: ["exp", "iat"],
clockTolerance: 30, // seconds of allowed clock skew
maxTokenAge: "15m", // also bound by issue time
});
jwt.decode(token, key, algorithms=["ES256"], audience=AUD, issuer=ISS,
leeway=30, options={"require": ["exp", "iat", "nbf"]})
Keep access token lifetimes short, minutes rather than days, and use refresh tokens with their own controls for longer sessions. Sync server clocks with NTP, and keep tolerance in seconds.
How to test it
- Mint a token with
expone minute in the past. Expect 401. - Mint one with
nbfin the future. Expect 401. - Mint one with no
exp. Expect 401. - Test the boundary: a token 10 seconds past expiry passes with 30 seconds of tolerance, one 60 seconds past does not.
- Search for
ignoreExpiration,verify_exp: Falseand similar flags.
Common mistakes
- Treating a missing
expas valid. - Large clock tolerance such as hours "to fix clock skew".
- Expiry in milliseconds where seconds are expected, giving tokens valid for thousands of years.
- Disabling expiry checks in tests and shipping the same config.
Quick check
A token has no exp claim. Should the API accept it?
Not unless you have a documented reason. Require exp so every token has an end.