A JWT is three chunks of base64url text joined by dots. The first is a header naming the algorithm that signed the token, the second is the payload — a plain JSON object of claims such as who the user is and when the token stops being valid — and the third is a signature over the first two. Only the signature needs a key. The header and the payload are encoded, not encrypted, so anyone holding the token can read every claim inside it, including the person it was issued to.
That last sentence is the one people get wrong. A JWT sitting in a browser's local storage is readable by the user, by any script on the page, and by anyone who gets hold of it afterwards. Encoding is not a lock.
What are the three parts of a JWT?
The header
Usually two fields. alg names the signing algorithm — HS256, RS256 and ES256 cover most of what you will meet in the wild — and typ is almost always the string JWT. Tokens from an identity provider often add kid, a key identifier that tells the verifier which of the issuer's several public keys to use.
The encoding is base64url: ordinary base64 with - and _ standing in for + and /, and the trailing = padding removed so the token can be dropped into a URL or a header without escaping. If that scheme is unfamiliar, base64 and what it is actually for is the shorter read to do first.
The payload
A JSON object whose keys are called claims. Some are standardised, most are whatever the issuer decided to put there: a user ID, an email address, a role, a tenant, a list of scopes.
You do not need a key or a library to look. Paste the token into the JWT decoder here and the header, the payload and every claim come out in a table, with the timestamps converted into real dates. It runs entirely in the browser and makes no network requests, which is the only sane way to look at a token that is still live.
The signature
Raw bytes, base64url-encoded, computed over the exact ASCII string header.payload — the encoded forms, not the JSON. Change one character in the payload and the signature no longer matches.
Its length narrows down the algorithm before you read the header. HS256 produces 32 bytes, which come out as 43 characters. ES256 produces 64 bytes, or 86 characters. RS256 with a 2048-bit key produces 256 bytes, or 342 characters — which is why RS256 tokens are so much bulkier than HS256 ones carrying identical claims. Length alone is not proof, though: HS512 also lands on 86 characters, so it and ES256 look identical from the outside.
Which claims are standard?
RFC 7519 registers seven, and every one of them is optional. A token with none of them is still a valid JWT.
iss— issuer. Who minted the token, usually a URL.sub— subject. Who or what the token is about, typically a user ID.aud— audience. Which application is meant to accept it.exp— expiry time.nbf— not before. The token is invalid until this moment.iat— issued at.jti— a unique ID for the token, used to detect replay.
Everything else is the issuer's own invention. OpenID Connect adds a familiar set — email, name, nonce, auth_time — and your own backend probably adds two or three more. There is no schema to validate against, so a decoder can only label the claims it recognises; a custom claim comes out with its raw value and no explanation, because there is nowhere to look one up.
Why do the timestamps look like nonsense?
exp, nbf and iat are NumericDate values: seconds since 1 January 1970 UTC. Seconds, not milliseconds. Unix timestamps and how to read one covers the format, but the JWT-specific trap is worth stating plainly.
JavaScript's Date.now() returns milliseconds. Put it straight into exp and you mint a token that claims to expire tens of thousands of years from now — today's millisecond count, read as seconds, lands somewhere past the year 58,000. Every verifier accepts it, nothing logs a warning, and your session length is now effectively infinite. The number is a thousand times too large and looks perfectly plausible at a glance — ten digits is a date this decade, thirteen digits is a bug.
The other thing to know about these fields is clock skew. Server clocks drift, so verifiers can be configured to allow a small tolerance in both directions, and 60 seconds is a common setting. Several popular libraries default to no tolerance at all, so check yours rather than assuming: a token your machine calls expired may still be accepted, and one that looks fresh may be rejected.
What does a valid signature actually prove?
One thing: that the header and payload have not been altered since someone holding the key signed them. That is genuinely useful and it is also the entire guarantee.
It does not prove who is presenting the token. A JWT is a bearer credential — whoever holds it can use it. A stolen token is a valid token, and the signature check will pass happily for the thief.
It does not prove the token was meant for you either. Checking the signature is not the same as accepting the token: you still have to confirm that iss is the issuer you trust and aud is your application. A correctly signed token issued for a different service is still the wrong token, and plenty of integrations have skipped that check.
The alg field is a claim, not a fact
The header is supplied by whoever sent the token, which means the attacker controls it too. Two well-known attacks come from trusting it. The first is alg: none: the JOSE specifications include an unsecured mode whose signature is the empty string, and a library that reads the algorithm out of the header will accept a token an attacker typed by hand. The second is downgrading RS256 to HS256 — if the server verifies whatever the header asks for, an attacker re-signs the token with HS256 using the server's own public key as the HMAC secret. The key is public, so the attack is free.
The fix in every library is the same: pin the algorithm you expect instead of reading it from the token.
Why can nobody cancel a token?
A JWT is self-contained by design. The server checks the signature and the timestamps and touches no database, which is the whole performance argument for using one. The cost is that logging someone out, stripping a role or banning an account cannot take effect until the token expires, because nothing is consulted at verification time.
Every workaround gives up part of the benefit. Short expiry plus a refresh token moves the database lookup to the refresh step. A deny list of revoked jti values puts the lookup back where you were avoiding it. A token-version claim compared against the user record does the same thing with fewer rows. There is no arrangement that keeps the statelessness and gets instant revocation.
What should never go in a JWT payload?
Anything you would not paste into a public chat. Internal user IDs, email addresses and role flags leak out of tokens constantly, because the people adding them are thinking of the payload as server-side data.
Size matters more than people expect, too. The token travels on every request, base64 adds about a third to whatever you encode, and a browser cookie is limited to roughly 4 KB. Stuff a full user profile and a list of permissions into the payload and you will eventually meet a proxy that rejects the request header rather than a clean error message.
What can a decoder not tell you?
Reading a token locally answers most questions, but be clear about what it cannot do. A browser-based decoder will not fetch the issuer's keys: a real verifier reads kid and pulls the matching key from a JWKS endpoint over the network, so here you have to paste the secret or public key yourself. It also has no opinion on whether iss and aud are the values your application should accept — that check only exists in your code.
Encrypted tokens are a different object entirely. A JWE has five parts instead of three and its payload is ciphertext, so there is nothing to read without the decryption key.
And one practical gotcha with HMAC secrets: some issuers store the secret base64-encoded and decode it before signing. If a secret that should work verifies nowhere, the bytes being hashed are the problem, not the token — decode the secret first, which in the decoder above is the "Secret is base64" checkbox.
If you have a token in front of you right now, the JWT decoder will lay out its header, payload and claims, convert the timestamps, flag an expired or unsigned token, and check the signature against a secret or public key you paste in. Nothing is sent anywhere, though a live token in your clipboard is already less private than one that stayed in an HTTP header.
The claim that trips people up most often is exp, and it is really a Unix timestamp problem rather than a JWT one. What a Unix timestamp is and how to read one explains why ten digits and thirteen digits mean wildly different dates, which is the difference between a one-hour session and one that never ends.
Frequently asked questions
What information is stored in a JWT token?
A header naming the signing algorithm, a payload of claims, and a signature. The claims typically include a user ID, an issuer, an audience and expiry and issued-at timestamps, plus whatever else the issuer added, such as an email address, a role or a list of scopes. There is no fixed schema, so two tokens from different services can look nothing alike.
Can anyone read a JWT without the secret?
Yes. The header and payload are base64url-encoded, which is a text format anyone can reverse in a second. The secret or public key is only needed to confirm that the contents have not been changed since they were signed, not to read them.
Is a JWT encrypted?
A standard signed JWT is not encrypted, only encoded and signed. There is a separate format, JWE, whose payload is genuinely ciphertext; you can tell them apart by counting the dots, since a JWE has five parts instead of three. If a value must stay secret, it does not belong in a signed token.
What does the signature on a JWT prove?
Only that the header and payload have not been altered since someone with the key signed them. It says nothing about who is presenting the token, so a stolen JWT passes verification exactly like a legitimate one. It also does not confirm that the issuer and audience are the ones your application expects, which is a separate check you have to make.
How long should a JWT be valid for?
Short, because a JWT cannot be cancelled before it expires. Access tokens are commonly given lifetimes of a few minutes up to an hour, with a longer-lived refresh token handling renewal so the revocation check happens there. The right number is a trade-off between how often you want to hit the database and how long a stolen token stays useful.
Why does my JWT never expire?
The usual cause is putting milliseconds into the exp claim. JWT timestamps are counted in seconds since 1 January 1970, so a thirteen-digit millisecond value is read as a date tens of thousands of years away. A ten-digit exp is a date in this decade; a thirteen-digit one is a bug.
Last updated September 19, 2026