JWT Claims
A claim is a key-value pair in a JWT’s payload. Some have agreed-on meanings everywhere. Others are whatever your app made up. Knowing which is which is what keeps your tokens portable.
Registered claims (RFC 7519)
Seven claims are defined in the core JWT spec. They are short on purpose; JWTs are small.
| Claim | Meaning |
|---|---|
iss | Issuer. The URL of the entity that minted the token. |
sub | Subject. The principal the token is about. For users, this is their stable ID. |
aud | Audience. The intended recipient(s). A string or array of strings. |
exp | Expiration time. Unix timestamp. After this, reject. |
nbf | Not before. Unix timestamp. Reject if current time is before this. |
iat | Issued at. Unix timestamp. |
jti | JWT ID. A unique identifier, useful for revocation lists. |
Always check exp, nbf, iss, and aud on incoming tokens. Skipping any one of these is a known class of bug, and people keep shipping it.
OIDC standard claims
OIDC adds a vocabulary for user identity:
Identity
name,given_name,family_name,middle_name,nicknamepreferred_usernameprofile,picture,websitegender,birthdatezoneinfo,localeupdated_at
Contact
email,email_verifiedphone_number,phone_number_verifiedaddress(a structured claim)
Authentication context
auth_time: when the user actually logged innonce: replay protection (ID Token only)acr,amr: authentication context class and methods used
These are returned in ID Tokens and at /userinfo, scoped by what the client requested (profile, email, etc.).
OAuth2 access-token claims
Access tokens that happen to be JWTs (the common case) often carry:
scope: space-separated list of granted scopesclient_id: which client requested the tokensub: the user, or the client itself forclient_credentialsroles/realm_access/resource_access: provider-specific role claims
These are not standardized across all providers, but they are very common.
Custom claims
You can add your own. Two rules of thumb:
Namespace them. A flat claim called tenant_id works in your app today. Tomorrow a library you import adds its own tenant_id, and the meanings collide. Prefix custom claims with a URL or reverse-domain string:
{
"https://example.com/tenant_id": "acme",
"https://example.com/feature_flags": ["beta-search"]
}
This is the OIDC convention for non-standard claims and avoids stepping on registered names.
Keep them small. Every claim ships in every HTTP request that carries the token. A 4 KB cookie header is a real cost. If a value is large or rarely needed, put it behind /userinfo instead.
Audience (aud) deserves a section
aud is the most-misunderstood standard claim. It says who this token is for.
- An ID Token’s
audis the client ID. The client checksaud == my_client_id. - An access token’s
audis the resource server. The API checksaudmatches its identifier.
If you have multiple APIs, each should validate that incoming tokens were minted for it. Otherwise a token issued for api-A can be replayed against api-B. This is the failure mode that ID-Token-as-bearer-token attacks exploit.
A practical validation checklist
For any incoming JWT:
- ✅ Signature verifies against the expected key.
- ✅
algis in your allow-list (nevernone). - ✅
issmatches the expected issuer. - ✅
audcontains your identifier. - ✅
expis in the future. - ✅
nbf(if present) is in the past. - ✅ Type-specific checks:
nonce(ID Token),scope(access token), etc.
In FerrisKey
FerrisKey lets you control which claims appear in tokens through protocol mappers on clients and scopes. You can attach standard OIDC claims, add custom ones, and configure their lifecycle.