Skip to content

JWT Expiry, Refresh, and Common Mistakes

Sep 2, 2026 · Security

A JWT carries its own expiry inside the signed payload, and getting that expiry, its refresh flow, and its validation right is what separates a safe token from a forgeable one. A JSON Web Token is three base64url-encoded parts joined by dots: a header, a payload of claims, and a signature. The header and payload are only encoded, not encrypted, so anyone can read them. That is why the security lives entirely in the signature and in how carefully the verifier checks the time-based claims. This article covers what exp, nbf, and iat mean, why access and refresh tokens are separated, and the mistakes that repeatedly turn JWT auth into a hole.

What do exp, nbf, and iat actually mean?

They are the three registered time claims, and each is a NumericDate: an integer count of seconds since the Unix epoch, 1970-01-01 00:00:00 UTC. A verifier reads them from the payload and compares them against the current time.

Claim Name Meaning
exp Expiration Time The token must be rejected at or after this instant.
nbf Not Before The token must be rejected before this instant.
iat Issued At When the token was created; used for age checks and auditing.

A decoded payload might look like this:

{
  "sub": "1024",
  "name": "Ada",
  "iat": 1735689600,
  "nbf": 1735689600,
  "exp": 1735693200
}

Here the token was issued and became valid at the same second and expires 3,600 seconds (one hour) later. All three claims are optional in the spec, but a token without exp effectively never expires, which is almost never what you want. Because these are absolute UTC timestamps, servers must have reasonably synchronised clocks; verifiers usually allow a small leeway (a few seconds) to absorb clock skew rather than rejecting a token that is off by a second. You can paste a token into the JWT decoder to read these values as human-readable dates, which makes an off-by-one-hour timezone bug or a missing exp obvious at a glance.

Why split into access tokens and refresh tokens?

Because the two jobs have opposite requirements: an access token needs to travel constantly, while a refresh token needs to be guarded. An access token is presented on every API call, typically in an Authorization: Bearer header. The more often a secret is sent, the more chances it has to leak through logs, proxies, or a compromised endpoint. So access tokens are made short-lived, often minutes to an hour, limiting the window in which a stolen one is useful.

A refresh token solves the resulting friction. Instead of forcing the user to log in again every hour, the client keeps a longer-lived refresh token and sends it to a dedicated token endpoint to mint a fresh access token when the old one expires.

Property Access token Refresh token
Lifetime Short (minutes–1 hour) Long (days–weeks)
Sent to Every protected API Only the auth/token endpoint
Frequency of use Constant Rare
If leaked Limited window High impact — treat as a credential

Because the refresh token is high value, it is stored more carefully (for example an HttpOnly, Secure cookie rather than JavaScript-readable storage) and can be rotated: each refresh issues a new refresh token and invalidates the old one, so a stolen-and-replayed refresh token can be detected when the legitimate client later presents the now-invalid version. The split also gives you a revocation lever the stateless access token lacks — you keep refresh-token state server-side, so cutting off a session means refusing to honour its refresh token.

How does a verifier decide a token is valid?

Verification is a fixed sequence, and skipping any step is where bugs live. A correct verifier does all of the following before trusting a single claim:

  1. Split the token and base64url-decode the header and payload.
  2. Read the alg from the header but do not trust it blindly — compare it against the algorithm you expect.
  3. Recompute the signature over header.payload with the correct key and confirm it matches the third segment.
  4. Check exp is in the future and nbf (if present) is in the past, allowing small leeway.
  5. Validate iss (issuer) and aud (audience) match what this service accepts.

Only after the signature checks out do the claims mean anything. A decoded-but-unverified payload is just attacker-controllable JSON. This is the single most important mental model for JWTs: reading a token and trusting a token are completely different acts.

What are the common JWT mistakes?

Most JWT failures are a handful of recurring errors, and each has a direct fix.

Issuing tokens with no expiry

A token without an exp claim is valid forever. If it leaks, there is no natural point at which it stops working, and because a plain JWT is stateless you often cannot revoke it either. Always set exp, keep it short, and use refresh tokens to extend sessions rather than issuing long-lived access tokens.

Accepting alg:none or a caller-chosen algorithm

The none algorithm means “unsigned”. Some libraries historically honoured it, which let an attacker send a token with no signature and any payload they liked. A related attack swaps a token signed with an RSA public key to appear as an HMAC token, tricking a verifier into using the public key as an HMAC secret. The defence is the same for both: pin the expected algorithm on the server and reject anything else, including none. Never let the token’s own header decide how it will be verified.

Storing tokens in localStorage

Anything in localStorage is readable by any JavaScript running on the page, so a single cross-site scripting flaw hands an attacker the token. For browser apps, an HttpOnly, Secure, SameSite cookie keeps the token out of reach of page scripts. If you must use localStorage, understand that you are betting the token’s safety on having zero XSS, which is a hard bet to win.

Trusting claims without verifying the signature

Reading role: admin from a decoded payload and acting on it, without checking the signature, lets anyone grant themselves any claim they type. Because the payload is just base64url, forging one is trivial. Every claim you rely on for authorization must come from a token whose signature you have verified with your key.

Putting secrets in the payload

The payload is encoded, not encrypted. Do not place passwords, full personal records, or API secrets in a JWT, because anyone who holds the token can read them. Put an identifier in the token and keep the sensitive data server-side.

How can I inspect a token safely?

Decode it, but never paste a production token into a tool that sends it anywhere. Decoding a JWT is pure base64url and JSON parsing, so it can happen entirely in your browser with no network call. That matters for tokens because they are live credentials: pasting one into a server-side “JWT viewer” means handing your access token to whoever runs that server. The JWT decoder runs in the page, so the token stays on your machine while you read its header, its claims, and its exp and iat as real dates. Use it to confirm the expiry is set and sensible, to check the algorithm matches what your service expects, and to spot a payload carrying data that should never have been in a token.

Key takeaways

JWT security is mostly discipline around a few facts. The token is readable by anyone, so its integrity depends entirely on a verified signature. The exp, nbf, and iat claims are UTC-epoch timestamps that let a verifier reason about time, and exp should always be present and short. Splitting access and refresh tokens limits blast radius when a token leaks and gives you a revocation path. And the classic mistakes — no expiry, alg:none, storing tokens where scripts can read them, and trusting unverified claims — are all avoidable with a strict, algorithm-pinned verification step. Decode freely to understand a token; trust it only after you have checked the signature.

Frequently asked questions

Where is the expiry stored in a JWT?

In the payload as the exp claim, a NumericDate holding the number of seconds since the Unix epoch (1970-01-01 UTC). A verifier compares it against the current time and rejects the token once exp has passed.

Is decoding a JWT the same as verifying it?

No. Decoding just base64url-decodes the header and payload so you can read them; anyone can do that. Verifying means checking the signature against a key and validating claims like exp. A decoder shows contents but proves nothing about authenticity.

What is the difference between an access token and a refresh token?

An access token is short-lived and sent with every API request. A refresh token is long-lived, stored more carefully, and used only to obtain new access tokens from the auth server. Splitting them limits the damage if an access token leaks.

Why is alg:none dangerous?

It tells the verifier the token is unsigned. If a library honours it, an attacker can forge any payload with no key at all. Verifiers must pin the expected algorithm and reject none.

Can I revoke a JWT before it expires?

Not by itself, because a standard JWT is stateless and valid until exp. Revocation needs extra state: a short expiry plus refresh tokens, a denylist of token IDs (jti), or a server-side session check.