When I decode a JWT, I do not read every claim in random order. I use a repeatable sequence so I can answer the most important debugging questions quickly: who issued this token, who is it for, is it still active, and what does the application think the subject is allowed to do?
I usually start with the JWT Decoder, then move from standardized claims to application-specific ones.
The order I use when I inspect a JWT payload
For a payload like:
{
"iss": "https://auth.example.com/",
"sub": "user_123",
"aud": "api://orders",
"scope": "orders:read orders:write",
"iat": 1789200000,
"exp": 1789203600
}
I normally inspect these first:
iss— who does the token claim issued it?aud— which recipient or API is it intended for?expandnbf— is it currently usable from a time perspective?iat— when was it issued?sub— who or what is the subject?scope,roles,groups— what authorization data is present?jti,azp,auth_time,acr,amr— what extra context is useful for this protocol?
This order helps me separate authentication problems from authorization problems.
How I use iss — issuer
iss identifies the issuer.
When I see:
{
"iss": "https://login.example.com/"
}
I compare it with the issuer the API is configured to trust. I do not trust it just because the URL looks familiar.
RFC 8725 makes the security requirement stronger than a simple string check: the cryptographic keys used to verify the JWT must belong to the issuer being trusted.
If I suspect an issuer mismatch, I use the JWT Debugger to compare the actual value with the expected value, then verify that my key source is tied to the same issuer.
How I use aud — audience
aud tells me who the token is intended for.
It can be a string:
{
"aud": "api://orders"
}
or an array:
{
"aud": ["api://orders", "api://reporting"]
}
If the Orders API expects api://orders but the token says api://billing, I have a strong explanation for a 401 even if the signature is valid.
This is one of the claims I inspect most often when dev, staging and production identity configurations get mixed. The JWT audience mismatch guide covers that troubleshooting flow in detail.
How I use exp — expiration
exp tells me when the token should stop being accepted.
I remember that JWT NumericDate values use seconds, not JavaScript milliseconds. If I want a quick readable timestamp, I open the JWT Expiry Checker.
I do not treat an unexpired exp as proof that the token is valid. It only tells me that one time condition has not failed yet.
The JWT exp guide explains how I check clock skew, missing expiration and unit mistakes.
How I use nbf — not before
nbf is the inverse time boundary: the token should not be accepted before that time.
When a token is not expired but is still rejected, I check whether nbf is in the future.
I also compare server clocks because a small difference between the issuer and verifier can matter near the activation boundary.
How I use iat — issued at
iat tells me when the token says it was issued.
I use it as context rather than as a universal validity rule. It helps me answer questions such as:
- does this token belong to the login session I am actually debugging?
- is the token unexpectedly old?
- does the declared lifetime between
iatandexplook reasonable? - is
iatstrangely far in the future?
I do not assume iat automatically creates an expiration window. That depends on application policy.
How I use sub — subject
sub identifies the subject represented by the token.
I avoid guessing what it means. It might be a user ID, service identity or another subject identifier depending on the issuer.
I also avoid combining sub values from unrelated issuers as though they share one global namespace. The pair of issuer and subject is often more meaningful than sub alone.
What I expect from jti
jti is a JWT ID.
When present, it can help a system identify one specific token. But I do not treat the presence of jti as automatic replay protection or revocation support. Those behaviors require application policy and, in many cases, server-side state.
How I treat scope, roles and groups
These claims are extremely useful during debugging, but I do not assume they have universal semantics.
For example:
{
"scope": "profile orders:read",
"roles": ["support", "billing"],
"groups": ["employees"]
}
One platform may use a space-delimited scope string. Another may use namespaced permission claims. A field called roles is not automatically authoritative merely because it is readable.
When authentication succeeds but an API returns 403, these are the claims I inspect next.
The extra claims I look for in OAuth and OIDC flows
Depending on the provider and token type, I may also inspect:
azp— authorized party;auth_time— authentication time;acr— authentication context class reference;amr— authentication methods references.
I use these only when the relevant protocol or provider documentation gives them meaning. A generic decoder can show the values, but it cannot know every application's business rules.
The rule I use before trusting any claim
I separate three questions:
- Decode: what does the payload say?
- Verify: does the signature match trusted key material?
- Validate: are issuer, audience, time claims and permissions acceptable here?
I never skip from step 1 to “this user is authenticated.”
The JWT Signature Verifier handles supported signature checks, while the JWT Validator and JWT Debugger help with common claim validation.
My practical claim-inspection checklist
When I am debugging a token, I use this order:
- I decode the JWT without trusting it yet.
- I compare
isswith the configured issuer. - I compare
audwith the current API. - I check
exp,nbfandiatas one timeline. - I identify
subin the context of the issuer. - I inspect scopes, roles and groups only after authentication checks make sense.
- I verify the signature using a key source tied to the trusted issuer.
- I check provider-specific claims against provider documentation.
This approach keeps me from getting distracted by a large payload. I focus first on the claims that explain trust, recipient, time and authorization, then move into application-specific details.