JSON Web Tokens (JWTs) show up everywhere: session cookies, Authorization Bearer headers, mobile app logins, and service-to-service auth. They are compact, URL-safe strings that carry claims about a user or client. They are also easy to misunderstand - especially the difference between decoding a token and verifying that it is trustworthy.
This guide explains the three parts of a JWT, how to inspect one without leaking secrets, and which security pitfalls bite teams in production.
Who this is for
Backend and frontend developers integrating OAuth or custom auth, mobile engineers debugging expired sessions, and anyone who has copied a long eyJ… string from DevTools and wondered what it contains. Security reviewers who need a clear mental model for token handling will also benefit.
What a JWT is (and is not)
A JWT is a signed (or sometimes encrypted) encoding of a JSON object. In the common signed form (JWS), the token asserts claims - subject id, roles, expiry - and a signature binds those claims to a key held by the issuer. Recipients who trust that issuer and verify the signature can accept the claims without a database round-trip on every request.
A JWT is not encryption by default. The payload of a typical signed JWT is only Base64URL-encoded, not hidden. Anyone who obtains the token can read the claims. Do not put passwords, full card numbers, or other secrets in the payload and assume the format protects them. If confidentiality matters, use encrypted JWTs (JWE) or keep sensitive data on the server.
Header, payload, and signature
A compact JWT has three segments separated by dots: header.payload.signature. Each of the first two segments is Base64URL-encoded JSON.
The header typically declares the token type (JWT) and the signing algorithm (for example HS256 or RS256). The payload holds claims: registered ones like sub, iss, aud, exp, iat, and whatever custom claims your app adds. The signature is computed over the encoded header and payload using the issuer’s key material.
When you decode for debugging, you are reversing the Base64URL encoding of header and payload. You are not proving the signature is valid unless you also verify with the correct key and algorithm checks.
How to inspect a token safely
Decoding is useful when a client “mysteriously” logs out, when roles look wrong, or when clock skew might be expiring tokens early. Paste the token into a local decoder, read exp and iat in human time, and confirm aud/iss match what your API expects.
Safety rules matter more than convenience. Prefer tools that run in the browser so the token is not uploaded to a third-party decoder. ToolMint’s JWT decoder is built for that local inspection workflow. Even then, treat tokens as credentials.
- Never paste production access tokens into random websites you do not trust
- Prefer staging or deliberately expired sample tokens when sharing with teammates
- Redact screenshots; JWTs in chat logs become replay material
- Clear decoder fields when you leave your desk on a shared machine
- Rotate tokens if you accidentally exposed a live production credential
Decoding vs verifying
Decoding answers: what claims does this string claim to carry? Verifying answers: did a trusted issuer sign this, with an allowed algorithm, for the audience we serve, and is it still within its lifetime?
Libraries that only Base64-decode without verifying are fine for human debugging and dangerous as an auth mechanism. Attackers can forge a payload if your server skips signature checks. Algorithm confusion attacks (for example accepting “none” or swapping RS256 for HS256 with a public key misuse) have caused real breaches when verification was implemented carelessly.
Production verification should use a maintained JWT library, pin allowed algorithms, validate iss/aud/exp, and fetch signing keys from a trusted JWKS endpoint when using asymmetric crypto. Do not roll your own crypto.
Security caveats teams still miss
JWTs are bearer tokens: possession is authority for their lifetime. If stolen from localStorage via XSS, they can be replayed until expiry. Prefer httpOnly secure cookies where your threat model allows, combine with short lifetimes, and refresh carefully.
- Long-lived JWTs without revocation strategy amplify theft impact
- Putting PII you cannot afford to leak into readable payloads
- Trusting client-side decoding alone for authorization decisions
- Logging full Authorization headers in centralized logs
- Ignoring clock skew between issuer and API servers
- Using the same signing secret across unrelated environments
A debugging playbook
When auth fails, decode the token locally and check expiry first. Confirm the audience and issuer. Compare the algorithm in the header with what your verifier allows. If the signature fails, check whether you are using the wrong JWKS kid or an outdated secret after a rotation.
If claims look correct but the API still rejects the call, the bug may be elsewhere - middleware order, missing HTTPS, or a gateway stripping headers. Decoding narrows the search; it does not replace server logs and verifier configuration review.
Refresh tokens and session design (briefly)
Access JWTs are often paired with refresh tokens. Keep access tokens short-lived so theft windows shrink; store refresh tokens more carefully and rotate them on use when your threat model requires it. Do not put refresh tokens in localStorage if XSS is a realistic risk for your app.
Opaque server-side sessions remain a valid alternative when you need immediate revocation. JWTs trade revocation simplicity for horizontal scaling. Choose deliberately rather than defaulting to JWTs because a tutorial used them.
Takeaway
JWTs package claims in a portable signed string. Read them to debug; verify them to trust. Keep production tokens out of untrusted decoders, assume payloads are visible, and implement verification with battle-tested libraries. Used carefully, JWTs simplify scalable auth; used casually, they create a false sense of security.