Decoding is not verifying
A JWT has three dot-separated segments: a header naming the algorithm, a payload of claims, and a signature. The first two are Base64url-encoded JSON — encoded, not encrypted — so anyone holding a token can read every claim in it. This tool decodes them in your browser, and so can any attacker who obtains the token.
The consequence is that a JWT payload must never contain anything secret. Putting a password, an internal identifier you rely on being unguessable, or personal data beyond what the client already knows into a token is equivalent to publishing it. Tokens end up in browser storage, server logs, proxy logs and error reports.
Verification is the separate and essential step: recomputing the signature over the header and payload using the correct key and confirming it matches. Only that proves the token was issued by whoever holds the key and has not been altered. Decoding tells you what a token claims; verifying tells you whether to believe it.
Two classic signature attacks
The alg: none attack exploits libraries that read the algorithm from the token's own header. An attacker rewrites the payload, sets the algorithm to none, strips the signature, and a naive verifier accepts it because the token declared that no signature was required. The defence is to decide the expected algorithm server-side and reject anything else, never to trust the header's claim about how to verify itself.
The algorithm-confusion attack is subtler. With RS256 the server verifies using a public key, which by definition is not secret. If the verifier accepts whatever algorithm the header names, an attacker can re-sign a forged token with HS256 — a symmetric algorithm — using the public key as the shared secret. The library, told to use HS256, verifies with that same public key and the forgery passes. Again the fix is pinning the algorithm.
Both attacks are old and both still appear, because the underlying design decision — putting the algorithm inside the token — invites the mistake. Treat the header as untrusted input, exactly like the payload.
Expiry, revocation and where to keep tokens
The registered claims worth checking are exp (expiry), nbf (not before) and iat (issued at), all Unix timestamps in seconds. Validate iss and aud as well: a token legitimately issued for one service should not be accepted by another, and failing to check the audience is a common cross-service authorisation bug.
The structural weakness of JWTs is revocation. A signed token is valid until it expires, and the server holds no state to consult, so logging out or disabling an account does not invalidate tokens already issued. The standard mitigation is short-lived access tokens — minutes, not days — paired with a longer-lived refresh token that is stored server-side and can genuinely be revoked.
Storage involves choosing between two attack classes. localStorage is readable by any JavaScript on the page, so a single cross-site scripting flaw exfiltrates the token. An httpOnly cookie is unreadable by JavaScript but is sent automatically, which reintroduces cross-site request forgery unless you set SameSite and use anti-CSRF tokens. The cookie approach is generally preferred, because XSS is both more common and more damaging.