Understand JWT token claims including exp, iat, nbf, iss, sub, aud. Covers clock skew, token refresh patterns, and common mistakes.
A JWT is three base64url-encoded segments separated by dots: header.payload.signature. The payload contains claims, which are key-value pairs that carry identity and authorization data. Every claim is visible to anyone who has the token. There is no encryption, only a signature that verifies the token was issued by a trusted party.
The claims fall into three categories:
iss, sub, aud, exp, nbf, iat, and jti.role, org_id, permissions).These three claims form a tokenβs lifecycle:
iat ββββββββββββ nbf ββββββββββββ token active ββββββββββββ exp
β β
βββ token starts being valid βββ token expires
iat (Issued At)
The Unix timestamp when the token was created. This is informational. It does not affect validation unless your application logic uses it to enforce a maximum token age.
{
"iat": 1735603200
}
1735603200 = January 1, 2025 at 00:00:00 UTC.
nbf (Not Before)
The Unix timestamp when the token becomes valid. If a token arrives with an nbf in the future, the server should reject it. This is rare in practice. Most systems set nbf equal to iat.
{
"nbf": 1735603200
}
exp (Expiration)
The Unix timestamp when the token becomes invalid. This is the key time claim. The server must reject any token with exp in the past.
{
"exp": 1735689600,
"iat": 1735603200
}
The difference is 86400 seconds (24 hours). This token has a one-day lifetime.
Beyond the time claims, the JWT spec defines several standard claims:
| Claim | Name | Purpose | Example |
|---|---|---|---|
iss |
Issuer | Who created the token | "https://api.example.com" |
sub |
Subject | Who the token is about | "user_12345" |
aud |
Audience | Who the token is for | "https://app.example.com" |
exp |
Expiration | When the token expires | 1735689600 |
nbf |
Not Before | When the token becomes valid | 1735603200 |
iat |
Issued At | When the token was created | 1735603200 |
jti |
JWT ID | Unique token identifier | "abc-123-def" |
iss (Issuer): Identifies who created the token. Your auth server should validate this to prevent tokens from other issuers from being accepted.
sub (Subject): The entity the token represents. Typically a user ID or service account name. Use this in your authorization logic to identify who is making the request.
aud (Audience): Who the token was issued for. If your system has multiple services (e.g., API gateway, user service, payment service), each should verify the aud claim matches its own identifier. This prevents a token intended for one service from being replayed against another.
jti (JWT ID): A unique identifier for the token. Use this for token revocation. Store the jti in a denylist when the user logs out or the token is revoked.
Serversβ clocks are never perfectly synchronized. If the auth serverβs clock is 30 seconds ahead of the API serverβs clock, a token with exp = 1735689600 might be validated against a server that thinks it is already 1735689631. The token is technically expired on the API server even though the auth server says it is valid.
The fix is a leeway window:
// Node.js with jsonwebtoken
jwt.verify(token, secret, { clockTolerance: 30 }); // 30 seconds tolerance
// Go with golang-jwt
token, err := jwt.Parse(tokenString, keyFunc,
jwt.WithLeeway(30 * time.Second),
)
A 30-60 second leeway is standard. Do not set it to zero or you will see intermittent 401 errors that are difficult to debug.
Using exp with seconds vs milliseconds
Some libraries use seconds (RFC 7519 standard), others use milliseconds. A token with exp: 1735689600 (seconds) looks like year 56935 if interpreted as milliseconds. Verify your libraryβs expectation.
Setting exp too far in the future
A token with a 30-day lifetime means a stolen token is usable for 30 days. Access tokens should expire in 15-60 minutes. Use refresh tokens for longer sessions.
Missing aud validation
If your API does not check the aud claim, a token issued for a different service can be used against your endpoint. Always validate iss and aud.
Storing sensitive data in claims
Claims are readable by anyone with the token. Do not put passwords, SSNs, or private keys in JWT claims. Use opaque references instead.
Paste the example token into JWT Decoder to see all claims decoded. The payload contains:
{
"iss": "https://api.example.com",
"sub": "user_12345",
"aud": "https://app.example.com",
"exp": 1735689600,
"iat": 1735603200,
"nbf": 1735603200,
"role": "admin",
"permissions": ["read", "write", "delete"]
}
The token was issued at midnight UTC on January 1, 2025, expires 24 hours later, and grants admin access to user_12345.
Nothing you paste leaves this tab. Every tool runs entirely in your browser β no upload, no server, no account.