JWT Claims Explained: iss, sub, aud, exp, nbf, iat and jti
The payload of a JSON Web Token is a set of claims: statements about the user and about the token itself. RFC 7519 reserves seven short names for the most common ones, and every serious JWT library knows how to check them. This guide gives the full form of each abbreviation, shows a real token, explains what a server must check for each claim, and covers the step that comes before all of them: verifying the signature, without which none of the claims mean anything.
The seven registered claims at a glance
The abbreviations are three letters long to keep tokens small, since a JWT travels in every request header. Their full forms, from RFC 7519 section 4.1:
iss issuer who created and signed the token
sub subject who the token is about, usually a user ID
aud audience who the token is intended for (string or array)
exp expiration time after this moment the token must be rejected
nbf not before before this moment the token must be rejected
iat issued at when the token was created
jti JWT ID unique identifier of this token
All seven are optional in the base standard. Profiles built on JWT make some of them mandatory: an OpenID Connect ID token must contain iss, sub, aud, exp and iat, and RFC 9068 does the same for OAuth access tokens in JWT format. Everything else in the payload, such as scope, email or roles, is a public or private claim defined by the system that issued the token.
jwks and kid are often mentioned in the same breath but are not claims. kid (key ID) is a header parameter, and a JWKS (JSON Web Key Set, RFC 7517) is the document of public keys, usually at a jwks_uri, that the server uses to check the signature.
A real token, decoded
Here is the payload of a token signed for this article, with times chosen so the arithmetic is easy to follow:
{
"scope": "orders:read",
"iss": "https://auth.example.com/",
"sub": "user_8f3a2c",
"aud": "https://api.example.com",
"iat": 1792400000, // 2026-10-19T08:53:20Z
"nbf": 1792400000,
"exp": 1792400900, // 15 minutes later
"jti": "4f1c9d2e"
}
Times are NumericDate values: whole seconds since 1970-01-01 UTC, not milliseconds. A common bug in JavaScript is writing exp: Date.now() + 900000, which produces a token valid for roughly 57,000 years, or comparing exp with Date.now() without dividing by 1000, which makes every token look expired. You can paste any token into the JWT decoder to see its claims with the timestamps converted to readable dates.
iss, sub and aud: who issued it, for whom, about whom
iss must exactly match the issuer you trust. It is compared as a case-sensitive string, so https://auth.example.com/ and https://auth.example.com without the trailing slash are different issuers, and libraries reject the token. Copy the value from the issuer's discovery document rather than typing it.
aud is the claim most often left unchecked, and skipping it is a real vulnerability. If two of your APIs trust the same identity provider and neither checks the audience, a token issued for the low-privilege API is accepted by the high-privilege one. Each API should require its own identifier. Note that aud may be a single string or an array; a token is acceptable when your identifier is one of the values.
sub identifies the principal, and it is unique only within its issuer. Store users by the pair (iss, sub), not by sub alone, and do not use the e-mail claim as a key, because e-mail addresses can change and be reassigned.
exp, nbf and iat: the time window
A token is usable only in the window from nbf to exp. iat records when it was created and is mostly informational, though some systems reject tokens that are too old by iat even if they have no exp. Clocks on different servers drift, so libraries allow a small leeway; RFC 7519 says it should usually be no more than a few minutes. Here is the same token checked with the jose library at different moments:
import { jwtVerify } from "jose";
await jwtVerify(jwt, key, {
issuer: "https://auth.example.com/",
audience: "https://api.example.com",
clockTolerance: 60, // seconds of leeway for exp and nbf
});
// 60 s after iat: valid
// aud "https://other..." ERR_JWT_CLAIM_VALIDATION_FAILED unexpected "aud" claim value
// issuer without slash ERR_JWT_CLAIM_VALIDATION_FAILED unexpected "iss" claim value
// 1000 s after iat: ERR_JWT_EXPIRED "exp" claim timestamp check failed
// 30 s before nbf: ERR_JWT_CLAIM_VALIDATION_FAILED "nbf" claim timestamp check failed
Keep access tokens short-lived, typically 5 to 60 minutes, and use refresh tokens for longer sessions. An exp far in the future turns a stolen token into a long-term key, because a stateless JWT cannot be revoked before it expires unless you keep a deny list.
jti and replay protection
jti is a unique ID for the token. On its own it does nothing; it becomes useful when the server remembers it. Storing the jti of each used token until its exp lets you reject a token that is presented twice, which matters for one-time tokens such as password-reset links, and it gives you something to put on a deny list when a user logs out or a token leaks. The issuer must generate it so that collisions are practically impossible, for example from a random UUID.
Signature first, claims second
Every claim check is meaningless if the signature was not verified, because anyone can create a token with any iss, aud or exp. The order a server must follow is: check that the algorithm is one you allow, verify the signature with the issuer's key, and only then validate the claims. Decoding the payload is not the same as validating it.
That is also why it is safe to read claims without a key for debugging. To see iss and aud of a request in browser DevTools, open the Network tab, select the API request, find the Authorization: Bearer … request header, and copy the token. The payload is the middle part between the two dots, Base64url-encoded. From a terminal:
python3 -c 'import sys,base64,json; p=sys.argv[1].split(".")[1]; print(json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4))))' "$TOKEN"
node -e 'const p=process.argv[1].split(".")[1]; console.log(JSON.parse(Buffer.from(p, "base64url")))' "$TOKEN"
The padding expression matters: Base64url in JWTs omits = padding, and Python's decoder refuses unpadded input. Plain base64 -d fails on many tokens for the same reason, and it does not understand the - and _ characters either. For signature checks during development, use the JWT validator, which verifies HS256, RS256 and other algorithms in the browser.
Sources and further reading
RFC 7519 defines JWT, the seven registered claims in section 4.1 and NumericDate. RFC 8725 (JWT Best Current Practices) covers audience checks, algorithm restrictions and other validation rules. RFC 7517 defines JSON Web Keys and key sets. OpenID Connect Core 1.0 section 2 lists the required ID token claims, and RFC 9068 the JWT profile for OAuth 2.0 access tokens. The IANA "JSON Web Token Claims" registry lists all registered claim names.
Frequently asked questions
What do iss, sub and aud stand for in a JWT?
iss is the issuer, the party that created and signed the token. sub is the subject, usually the user ID. aud is the audience, the API or application the token is intended for.
What is the difference between exp, nbf and iat?
exp is the time after which the token must be rejected, nbf the time before which it must be rejected, and iat the time it was issued. All three are seconds since 1970-01-01 UTC.
Are JWT claims required?
In RFC 7519 every registered claim is optional. Standards built on it make some mandatory: OpenID Connect ID tokens must contain iss, sub, aud, exp and iat.
Can aud be an array?
Yes. aud can be a single string or an array of strings. A server should accept the token only if its own identifier is one of the values.
Is jwks a JWT claim?
No. A JWKS is a JSON document listing the issuer's public keys, used to verify the signature. The kid header parameter tells the server which key in the set to use.
How do I see JWT claims in browser DevTools?
Open the Network tab, select the request, and copy the token from the Authorization: Bearer header. Paste it into a JWT decoder or decode the middle part with Base64url to see the claims.