Developer Cluster
Understanding JWT Tokens Safely: A Security Guide
A JWT is not a secret. That single sentence would have prevented a large fraction of the JWT-related bugs that have shipped to production in the last decade. Developers see a token that looks like random gibberish and assume encryption. It is not encryption. It is signed, base64url-encoded JSON, which means anyone who holds the token can read every field inside it — and if the signature is checked incorrectly on the server, anyone who holds a token can also forge one.
This guide is for developers who need to work with JWTs day to day: debug one from a failing API, decide which algorithm to pick, and avoid the pitfalls that have put major companies on security blogs. We will decode tokens, dissect the three parts, cover the algorithms that matter in 2026, and walk through the four classic mistakes.
What a JWT actually is
JSON Web Token is defined in RFC 7519. Strip away the jargon and a JWT is a way to send a small, signed JSON payload between two parties. The signature proves the payload has not been tampered with. The encoding is base64url so it can ride in an HTTP header or a URL without extra escaping. Nothing is encrypted by default. If your token says {"role":"admin"}, the text "admin" is sitting there in plain view, just one base64 decode away.
The point of the JWT is portability. A server can hand the token to a client, the client sends it back with every request, and the server validates the signature without needing to look up session state in a database. That is the attractive property. The cost is that once a token is issued, you cannot easily revoke it — more on that later.
Anatomy: header, payload, signature
A JWT is three base64url segments joined by dots. Copy this one into a decoder and see for yourself:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJkZW1vIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.dQw4w9WgXcQexampleSignatureBytes
Header
The first segment, decoded, is a JSON object describing how the token was signed. The two keys that matter are alg (algorithm — HS256, RS256, ES256, EdDSA) and typ (always "JWT"). The algorithm field is the single most dangerous part of a JWT. We will come back to it.
Payload
The second segment is the actual claims — the data the token carries. RFC 7519 reserves a handful of standard fields: iss (issuer), sub (subject — usually the user ID), aud (audience), exp (expiration timestamp), nbf (not before), iat (issued at), and jti (JWT ID, useful for revocation). Beyond these, applications add their own claims — role, tenant, permissions, whatever the API needs.
Important: never put secrets in a payload. It is readable. Never put PII beyond what is strictly needed. The payload is the part an attacker who steals the token will use to profile your users.
Signature
The third segment is the signature, computed over base64url(header) + "." + base64url(payload) using the algorithm named in the header. For HS256 this is an HMAC with a shared secret. For RS256 it is an RSA signature with a private key. The verifier recomputes the signature with the same key and compares. If it matches, the token has not been modified. If not, it is rejected.
Decoding a JWT safely in the browser
Debugging a JWT means staring at its claims. You want to see the role, the expiration, the issuer — all without sending the token anywhere, because a valid JWT is effectively a credential. Pasting a production token into a random "JWT decoder" website is how real incidents start.
Use JWT Decoder. It runs fully in your browser, splits the token into segments, and base64url-decodes each part into readable JSON. Open DevTools → Network if you want to verify nothing is sent — the only traffic should be the initial page load. Because decoding is unsigned, you see the claims regardless of whether the signature is valid. That is exactly what you want for debugging; you can tell "the token has the right user but wrong scope" without giving anyone the secret.
Need to inspect the component parts manually? Base64 Encoder/Decoder handles the raw segments, and JSON Formatter pretty-prints the decoded payload. For checking signatures by hand (rare, but useful in CTFs), the Hash Generator computes HMAC and SHA variants, and HMAC Generator handles keyed message authentication directly.
If you are building test tokens for development, JWT Generator produces valid tokens from a payload and key, and JWT Debugger lets you inspect claims with validation context. For quick UUID subjects and random test values inside payloads, UUID Generator handles that without network round-trips.
Algorithms in 2026
HS256 (HMAC-SHA256)
Symmetric. The same secret signs and verifies. Fast, simple, and appropriate when one system both issues and validates the token. The key must be long — at least 32 bytes of high-entropy random data. If you are using a hardcoded string like "secret123" (as many tutorials unfortunately show), your tokens can be forged in seconds with a dictionary attack.
RS256 / RS384 / RS512
Asymmetric RSA signing. The issuer holds a private key; verifiers use the public key. Appropriate when the issuer and verifier are different systems — you can publish the public key and let anyone verify without handing out the private key. RS256 is the workhorse of OIDC deployments.
ES256 / EdDSA
Elliptic-curve signatures. Smaller signatures and faster verification than RSA. ES256 uses P-256; EdDSA uses Ed25519. Both are modern, recommended choices for new systems per the current RFC 8037 guidance. If you are starting fresh, pick EdDSA.
none (do not use, ever)
RFC 7519 defines an "alg": "none" value that means "no signature." It is in the spec for legitimate reasons but has caused countless real breaches because naive verifiers accept it. We will cover this in the pitfalls.
Four pitfalls that have broken real systems
Pitfall 1: trusting the alg header
The classic bug: a library reads the algorithm from the token's own header and uses that to verify. An attacker changes "alg":"RS256" to "alg":"HS256", signs the token with the public key as if it were an HMAC secret, and the server happily accepts it. The fix is to pin the algorithm at the verifier — tell your library "I only accept RS256 for this key" and reject everything else. The OWASP JWT Cheat Sheet has been warning about this since 2015.
Pitfall 2: alg=none acceptance
The "none" algorithm means "trust whatever is in the payload." Older JWT libraries happily validated tokens with no signature if the caller did not explicitly disable it. Modern libraries reject "none" by default, but if you maintain legacy code, check. Better: explicitly allowlist the algorithms you accept and reject everything else, including "none," at load time.
Pitfall 3: forgetting to check expiration
Just because the signature validates does not mean the token is still valid. You must check exp. Some libraries do this automatically; others make it opt-in. Read your library's docs. Issued-at (iat) and not-before (nbf) should also be checked where relevant.
Pitfall 4: long-lived tokens with no revocation plan
JWTs are hard to revoke. That is their defining trade-off. If you issue a 30-day token and the user logs out, the token is still valid on every server that trusts it until it expires. The mitigations: keep access tokens short (5–15 minutes), use refresh tokens with a server-side blacklist for logout, and track jti so you can revoke individual tokens in an emergency. For anything long-lived, a stateful session cookie may actually be simpler.
When a JWT is the wrong choice
JWTs are not universal. They are great when:
- You have multiple services that need to verify the same token without shared session state.
- The payload is small and the data is short-lived.
- You can enforce short expirations.
They are a bad fit when:
- You need to revoke tokens instantly.
- The payload is growing past a few kilobytes — HTTP header size matters.
- You are putting sensitive data in the claims. Use a reference token instead (an opaque ID that the server looks up).
For many single-server applications, a traditional signed session cookie remains the simpler, safer choice. The OAuth 2.0 framework uses opaque access tokens by default; JWTs are an extension, not the default. For secrets at rest and high-entropy keys, Password Generator produces cryptographically strong values in the browser using the Web Crypto API.
Related pillar guide
This cluster post is part of the security track. For the broader foundation — threats, hashing vs encryption, OWASP, and the full toolkit — see Web Security Tools: A Beginner's Masterclass.
FAQ
Is a JWT encrypted?
No. A JWT is signed, not encrypted. The payload is readable by anyone holding the token. If you need confidentiality, use JWE (JSON Web Encryption), defined in RFC 7516. JWE is a separate standard in the same family.
Can I put an email address in the payload?
You can, but think about who sees the token. It ends up in browser storage, server logs, and proxy logs. If you would not log the email at INFO level, you probably should not put it in a JWT. Use the sub claim with an opaque user ID and look up details server-side.
How do I rotate the signing key?
Use the kid (key ID) header claim. The issuer signs with a key, tags the header with the key ID, and the verifier looks up which public key to use by ID. When you add a new key, verifiers accept both during the rotation window. When old tokens have all expired, you retire the old key.
What's the difference between a JWT and an OAuth access token?
OAuth 2.0 defines the flow for issuing access tokens. The token itself can be opaque (a random string the server looks up) or a JWT (self-contained). Many modern identity providers issue JWTs as their OAuth access tokens, but it is not required by the OAuth spec.
Should I implement JWT myself?
No. Use a library audited for your language — jose for Node, PyJWT for Python, jjwt for Java. The failure modes are subtle, the performance optimizations matter, and rolling your own is how the pitfalls above happen. The jwt.io library list tracks which libraries pass which conformance tests.
Closing thought
JWTs are a sharp tool. Used with care they simplify distributed auth. Used without care they ship a forgeable credential to production. The single most important habit is to stop thinking of a JWT as a secret blob and start thinking of it as a signed, readable document. Decode one right now in your browser, read the claims out loud, and notice that anyone who holds it can do the same. That is the entire security model in one sentence.