The three segments
The header is a small JSON object naming the signing algorithm in `alg` and the token type in `typ`. It often carries a `kid`, a key identifier telling the verifier which of several public keys to use, useful when a provider rotates keys, and the first thing to check when verification fails against a key you believe is correct.
The payload holds the claims. Some are registered by the JWT specification and mean the same thing everywhere; the rest are whatever the issuer chose to include. Both are just JSON, so a payload can be any size, which is the usual reason a token grows large enough to hit a header size limit.
For a signed compact JWS, the signature is computed over the encoded header and payload together. Because it covers the encoded form rather than the decoded one, re-serialising the JSON and re-encoding it produces a different string and invalidates the signature, which is why a token must be passed around exactly as issued, byte for byte. This decoder does not verify that signature.
The registered claims that matter
`exp` is the expiry, `iat` the issue time, and `nbf` the earliest time the token is valid. All three are Unix timestamps in seconds, not milliseconds, the single most common mistake when generating tokens in JavaScript, where `Date.now()` returns milliseconds and produces an expiry roughly fifty thousand years in the future.
`iss` names the issuer and `aud` the intended audience. A verifier is supposed to check both: a token legitimately issued by the right provider for a different application is still not valid for yours, and accepting it anyway is a real vulnerability rather than a technicality.
`sub` identifies the subject, usually the user, and `jti` is a unique token id used for revocation lists. Neither is required, and how a given system populates `sub` varies enough that it is worth confirming rather than assuming it is a database id.
Decoding is not verifying
Decoding reads the token. Verifying checks the signature against the issuer's key, and confirms the expiry, the issuer and the audience. A decoder can show you a token whose signature is complete nonsense and the claims will display perfectly, because reading Base64 does not involve the key at all.
This is why no client application should make a trust decision from a decoded payload. Reading `role: admin` from a token in browser JavaScript tells you what the token says, and a token can say anything if nobody checked the signature. Authorisation belongs on the server, after verification.
Using a decoded payload for display is fine and normal, showing a user's name from the token rather than making another request. The line is between displaying a claim and relying on it.
Algorithms, and the attacks on them
HS256 is symmetric: the same secret signs and verifies, so every party that can verify can also forge. RS256 and ES256 are asymmetric (a private key signs, a public key verifies), which is what you want when tokens are verified by services that should not be able to issue them.
The `alg: none` attack is the historical reason to be careful. A token claiming no algorithm, with an empty signature, was accepted by early libraries that trusted the header to say how to verify. A verifier must decide the expected algorithm itself and reject anything else, rather than reading it from the token it is checking.
The related confusion attack takes an RS256 system, changes the header to HS256, and signs with the public key as the HMAC secret, which works if the library picks its algorithm from the header and the public key is, as intended, public. Both attacks are fixed in current libraries and both are worth recognising in a token that looks odd.
What should never be in a payload
Because the payload is readable by anyone holding the token, it must not contain anything confidential: no passwords, no API keys, no personal data beyond what the application needs, no internal identifiers you would not expose. This happens regularly, and Base64 looking opaque is exactly why.
Tokens are also frequently stored in places that leak, browser local storage, log files, error reports, analytics payloads, and URLs. A token in a query string ends up in server logs and browser history, and a token in local storage is readable by any script that achieves XSS on the page.
Size is the practical constraint that follows. Every claim is sent with every request, and many servers cap header size at 8 KB; a token stuffed with permissions can exceed it and produce a confusing 431 or 400 rather than an authentication error.