Validating JWT time claims: exp, iat and nbf
DDEVELOPER
DeveloperPublished: 7 min read

Validating JWT time claims: exp, iat and nbf

An API rejects a token as soon as you issue it. Before extending its lifetime, check whether the verifier sees it as expired or too early to use. A verifier whose clock runs slow can see a fresh token as too early to use, even with an hour left before expiration. Here are five ways the timestamps in a signed JSON Web Token can lead an implementation astray.

Japanese original published: 2026-04-17

Read the timestamps in seconds

A JSON Web Token (JWT) carries claims from an issuer to a recipient. A claim is a named value: in an invitation, sub identifies the invited user and exp gives the deadline. Reading those values tells you what the issuer is claiming; the API still has to verify them.

The examples use the compact JWS representation. Its three dot-separated parts are the header, the payload (a JSON object containing the claims), and the signature value, each encoded with Base64url. Anyone holding this token can decode the first two parts without a key. JWTs can also use the encrypted JWE representation.

With HS256, the issuer computes an HMAC-SHA-256 over the encoded header, a period, and the encoded payload, using a shared secret key. The verifier’s library computes the same MAC and checks it against the third part. Editing exp breaks that check. Letting time pass leaves the MAC intact, so the API must also check the intended use, the recipient and its own clock. Anyone with the shared key can mint tokens; keep it out of the invitee’s browser.

RFC 7519 uses NumericDate: seconds since the Unix epoch, ignoring leap seconds. Fractional seconds are allowed. JavaScript’s Date.now() returns milliseconds, so use Math.floor(Date.now() / 1000) for an integer issued-at time. RFC 7519 makes exp, iat and nbf optional. Each application decides which it requires.

Scroll horizontally if the table does not fit.

ClaimNameWhat it tells the verifier
expExpiration timeReject at or after this instant, with zero clock tolerance.
iatIssued atWhen the issuer created this token.
nbfNot beforeThe first instant the token may be used, with zero tolerance.
auth_timeAuthentication time (OpenID Connect)When the end user authenticated.

Use this sample payload to follow the clock comparisons. The invitation example below also checks the signature and who the token is for.

{
  "sub": "user_123",
  "iat": 1713340800,
  "nbf": 1713340800,
  "exp": 1713344400
}

1. Distinguish an early token from an expired one

The sample becomes usable when issued and expires one hour later. At issuance, a verifier five seconds fast accepts it. A verifier five seconds slow rejects it because its clock has yet to reach nbf. Both results assume zero tolerance and valid credentials. If the error says “expired,” compare the verifier’s time with exp, in seconds.

These boundaries follow RFC 7519 §§4.1.4–4.1.5 and were tested in jsonwebtoken 9.0.2 and jose 6.1.0. The RFC permits a small allowance for clock skew. The 30-second column shows how these two versions implement that allowance.

Scroll horizontally if the table does not fit.

Verifier’s timeZero tolerance30-second tolerance
30 seconds before nbfToo earlyAccepted
Exactly nbfAcceptedAccepted
Exactly expExpiredAccepted
30 seconds after expExpiredExpired

Both jsonwebtoken’s verify and jose’s jwtVerify accept clockTolerance: 30. Increasing tolerance admits tokens earlier as well as later. Monitor clock synchronization and choose a tolerance that your application can afford on both sides.

2. Separate token age from time since sign-in

A token issued just now may belong to someone who signed in yesterday. To request recent authentication in OpenID Connect, send max_age in the authentication request. The resulting ID token must include auth_time, which the client can use to check how much time has passed since authentication.

In both library versions above, a signed token with iat 120 seconds in the future passed when other checks succeeded and no maximum token age was set. With a maximum age of 300 seconds and zero tolerance, jsonwebtoken’s maxAge still accepted it, while jose’s maxTokenAge rejected it. Test your chosen verifier if your policy rules out future issue times. Define the lifetime of the session separately from that of each token issued during it.

3. Start a scheduled invitation’s lifetime at release

Suppose an invitation opens eight days from now and should work for seven days after that. In jsonwebtoken, expiresIn: "7d" normally counts from issuance. If you issue the token today with that option, it expires on day seven, a full day before the invitation opens. Derive both boundaries from its release date instead.

// jwt is jsonwebtoken. releaseDate is a Date; secret is a securely stored
// random HS256 shared key of at least 256 bits.
if (typeof userId !== "string" || !userId) throw new Error("Recipient required");
const nbf = Math.ceil(releaseDate.getTime() / 1000);
if (!Number.isFinite(nbf)) throw new Error("Invalid release date");
const exp = nbf + 7 * 24 * 60 * 60;
const token = jwt.sign({ sub: userId, nbf, exp }, secret, {
  algorithm: "HS256", issuer: "invite-service", audience: "invite-api"
});

The example rounds a release time with fractional seconds up to the next whole second, then counts seven days from there. Rounding down could open the invitation up to 999 milliseconds early. With zero tolerance, the verifier accepts it starting at nbf and stops at exp. Keep the verifier’s clock synchronized: even zero tolerance allows early access in real time if that clock runs fast.

4. Distinguish refresh token rotation from session expiry

A JWT’s exp is an absolute timestamp. Using the token or refreshing a session leaves the timestamp in that token unchanged. An OAuth refresh token can also be an opaque string, with last-use time and revocation state kept on the server. An idle timeout and an absolute deadline measured from sign-in serve different purposes.

Rotation detects reuse. At each refresh, the authorization server issues a new refresh token, invalidates the old one and retains their relationship. If the invalidated token is presented again, the server revokes the active refresh token in that family. RFC 9700 requires authorization servers issuing refresh tokens to public clients to use sender-constrained refresh tokens or rotation to detect replay.

5. Verify the signed claims before accepting the invitation

A decoder can display a payload even if its signature is invalid. Change only exp and a decoder shows the new deadline, but verification against the original signature fails. Use the trusted issuer’s key and specify the algorithm, issuer and audience your endpoint expects.

// Get sessionUserId from the authenticated session.
if (typeof sessionUserId !== "string" || !sessionUserId) {
  throw new Error("Sign-in required");
}
const claims = jwt.verify(token, secret, {
  algorithms: ["HS256"], issuer: "invite-service", audience: "invite-api",
  subject: sessionUserId, clockTolerance: 0
});
if (!Number.isFinite(claims.exp) || !Number.isFinite(claims.nbf)) {
  throw new Error("Invitation requires exp and nbf");
}
if (claims.exp - claims.nbf > 7 * 24 * 60 * 60) {
  throw new Error("Invitation lifetime exceeds seven days");
}

Even a trusted issuer can accidentally sign a longer invitation. This API enforces the seven-day cap itself. It reserves the audience invite-api for invitations and checks that sub matches the signed-in recipient. If one issuer and audience handle multiple token types, define distinct validation rules for each use.

Checking a timestamp when present and requiring it to exist are separate operations. In jose, requiredClaims: ["exp", "nbf"] requires both claims. Keep allowed algorithms and key sources under the verifier’s control: an unverified header must never dictate which algorithms or key servers to trust.

Checks to run in your implementation

  • Agree on which claims are required and whether there is a maximum token age.
  • In tests, set the verifier’s clock to a known time and check just before and exactly at each boundary.
  • First change the payload without re-signing it. Then sign separate test tokens with the trusted test key: use the wrong audience in one and omit one required claim at a time in the others. Check that each token fails for the intended reason.
  • When rotating public keys, use the key ID (kid) to select a key from the trusted issuer’s key set. Keep validation rules specific to the token’s purpose.

Authorization after token verification

Once the service accepts a token, it still has to decide whether to allow the requested operation. Unix permissions enforce a separate boundary on the server itself. For instance, OpenSSH can reject a private key accessible to other users; mode 600 is a common fix. See the guide to Unix file permissions for the meaning of 600, 644 and 755.

References and sources

Editorial note

This article was prepared with AI assistance and reviewed by an editor before publication. It may still contain factual errors, interpretation mistakes, or outdated information. Check the cited primary sources or official documentation before making an important decision.

Related tools

Related articles