JSON Web Tokens (JWTs) turn up nearly everywhere authentication does: API access tokens, single sign-on, session state in stateless services. The format itself is small and mechanical. Once you know what the three segments contain, most JWT confusion comes down to two questions. What does this token say? And can you trust that it says it? This page covers both, along with the tools on this site for reading, creating, and debugging tokens.
What a JWT is made of
A JWT is three parts joined by dots: header.payload.signature. The first two parts are JSON objects encoded with base64url, which is not the same alphabet as ordinary Base64. Standard Base64 uses + and / as two of its 64 characters, and both mean something in a URL (they can trigger escaping or get misread by a query-string parser). JWTs use the URL-safe variant instead, with - and _ in their place and the trailing = padding dropped.
That difference matters. Give a standard-Base64 decoder a raw JWT segment and it can fail, or quietly return garbage, on any segment that contains a - or _. See Base64 vs Base64URL for the places this distinction causes trouble.
The header names the signing algorithm and the token type, typically {"alg":"HS256","typ":"JWT"}. HS256 is common, and so are RS256 and other asymmetric algorithms. The payload holds the claims, which is whatever data the issuer chose to put there.
Neither segment is encrypted. Base64url is an encoding, so anyone who has the token can decode the header and payload and read them. That's by design, but it means a JWT is the wrong place for a secret, a password, or anything you wouldn't want the token holder to see. The third segment, the signature, is what makes the token trustworthy. It's a cryptographic signature over the first two segments, computed with a key that only the issuer (and whoever it shares the key with) holds.
Common payload claims
RFC 7519, the JWT spec, defines a handful of standard claim names. All are optional, and most tokens use several: sub (the subject, usually a user or account ID), iss (the issuer, who created the token), and aud (the audience, who the token is meant for). Three timestamp claims matter a lot in practice. iat is when the token was issued. exp is the expiry: after this Unix timestamp the token should be rejected no matter how valid the signature is. nbf means "not before," so the token isn't valid until that time, which is useful for tokens issued ahead of when they should start working.
The encoding doesn't enforce any of this. A decoder will happily show you an exp from three years ago. Enforcing expiry is the verifying party's job, which is another side of the decode-versus- verify distinction below.
Decoding a JWT doesn't verify it
This is the most important thing to understand about JWTs, and the one people mix up most. The JWT Decoder on this site, like the debugger on jwt.io and every other decode-only tool, reads the header and payload and shows you what they say. It can't tell you whether the token is genuine. Checking the signature requires the secret or public key the issuer used. A client-side decoder has no way to get that key, and asking you to paste it in would be a security problem of its own.
Decoding tells you what the token claims. Verifying tells you whether to trust those claims, and only someone holding the right key can do it. The guide to decoding a JWT covers this in more detail, including why an attacker can edit and re-encode the payload of any JWT they intercept. Making the edit is trivial. What stops them is that the edited token no longer has a valid signature for its new content, so a server that verifies signatures rejects it.
A related mistake: pasting a token that still starts with Bearer (the HTTP Authorization header convention) into a decoder that expects the bare token. You get a confusing parse error that makes the token look broken, when the problem is seven extra characters at the front.
Algorithm-confusion attacks
The decode/verify gap is the root cause of two well-documented classes of JWT vulnerability. Both come from the same mistake: letting the token's own alg header decide how the token gets verified, instead of having the verifying code decide in advance which algorithm it expects and reject everything else. Details matter here, and a slightly wrong summary can do more harm than none. Read what follows as an explanation of the vulnerability class, and rely on your library's security advisories for specifics.
The alg: none attack. Some early JWT libraries, given a header of {"alg":"none"}, took it as an instruction to skip signature verification, treating "none" as a legitimate algorithm instead of a red flag. An attacker with a legitimate token could rewrite the payload (change a role claim to admin, or the subject to someone else's account), set the header to alg: none, drop the signature segment or leave it empty, and hand the result to a vulnerable verifier. The verifier would follow the header and skip the one check that would have caught the tampering. Maintained JWT libraries now reject none by default, and that's the reason: the header must never get to decide whether verification happens.
RS256/HS256 key confusion. This one is subtler and has hit production systems more recently than alg: none. RS256 is asymmetric: the server signs tokens with a private key and publishes the matching public key (often openly, at a JWKS endpoint) so clients and other services can verify tokens without holding the private key. HS256 is symmetric: one secret both signs and verifies.
The attack targets a server built to accept RS256 tokens and verify them against its public key, where the verification code is generic enough to also accept an attacker-supplied alg: HS256 header. The server then checks the signature using whatever key it has, which is the public key, now treated as an HMAC secret. The public key isn't secret by design, so an attacker who has it can compute a valid HMAC-SHA256 signature over any payload they like, using the public key as the HMAC key. The naive verifier, told by the header to treat the token as HS256, accepts it.
The fix on the verifying side follows the same principle as the alg: none case. The verifier has to specify the expected algorithm (and, for asymmetric algorithms, the expected key type) itself, and reject any token whose header says otherwise, instead of branching on whatever the token claims to be.
The tools on this site can't detect either of these. They're bugs in server-side verification code, not properties of a token you can spot by decoding it. You should know about them because they should shape how you configure the JWT library you verify with: pin the accepted algorithm(s) yourself, and be suspicious of any library default that infers behavior from the token's own header.
Signing a token
The JWT Encoder builds and signs an HS256 token from a header, payload, and secret you supply. It's handy for making a test token to paste into a client, a Postman request, or a local dev environment without running a real auth server.
HS256 is symmetric: the same secret signs and verifies, so whoever holds it can mint valid tokens as well as check them. Treat that secret like a password, and never carry a test secret into a production config. The HS256 signing guide walks through the process step by step, including how the signature is computed over the encoded header and payload, and why changing one character anywhere in either segment breaks the signature completely rather than leaving it a near match.
How long the secret should be
HS256 is built on SHA-256, which produces a 256-bit digest. RFC 2104 (the HMAC spec) discourages keys shorter than the hash output, and RFC 7518, which defines HS256 for JWTs, makes it a requirement: the key MUST be at least 256 bits. A short or guessable secret, like "test123", a dictionary word, or anything a person would type from memory, undermines the whole scheme no matter how sound the algorithm is. If an attacker can guess or brute-force the secret, they can forge tokens that pass verification. The weak point was the key, not the algorithm.
The JWT Secret Generator produces a cryptographically random secret with the Web Crypto API, 256 bits by default, in hex or base64url, depending on what your environment or config format expects. Going beyond 256 bits doesn't add meaningful security for HS256 (the hash output is the effective ceiling), but it doesn't hurt if a library or policy asks for it.
"jwt malformed" and other decode failures
When a decoder rejects a token outright, instead of showing you claims (right or wrong), the token usually isn't structurally a JWT. Maybe it's missing a segment, has stray whitespace, lost a character in a copy-paste, or still has the Bearer prefix. That's a different failure from an invalid signature. A malformed token fails before verification can even start, because there's no well-formed payload to check a signature against.
The "jwt malformed" guide goes through the causes in order of how often they come up. A corrupted token is one of the least likely. A copy-paste slip or a leftover prefix is much more common.
JWTs and the other ways to hold auth state
A JWT is one way to represent "who sent this request, and are they allowed to do this?" Session cookies backed by server-side storage and opaque bearer tokens (random strings with no embedded meaning, looked up in a database on every request) solve the same problem with different trade-offs around revocation, statelessness, and payload size.
A JWT can't be revoked before it expires without extra infrastructure, such as a blocklist or short expiries plus refresh tokens. Verification only checks the signature and the expiry claim. It doesn't ask whether the session still exists, which a database-backed session answers by default. Picking between these is an architectural decision, so work through it on purpose rather than using whatever the tutorial you followed happened to use. The JWT vs session cookies vs opaque tokens guide compares them in more depth.
Access tokens plus refresh tokens
A JWT stays valid until its exp claim runs out, and there's no built-in way to invalidate it sooner. That limitation is why most production systems using JWTs end up with the same pattern: a short-lived JWT access token (often 5–15 minutes) paired with a longer-lived, opaque refresh token that's checked against server-side storage every time it's used.
The access token handles authorization on each request. It's cheap to verify anywhere that has the public key or shared secret, with no database round-trip, which is the stateless property that makes JWTs appealing. Because it expires quickly, a leaked access token has a small, bounded window of exposure even though you can't revoke it individually.
The refresh token is the opposite on purpose. It's opaque (a random identifier with no claims to decode), it lives longer, and it's looked up in a database, which is what makes it revocable. When the access token expires, the client presents the refresh token to get a new one. Refresh token rotation strengthens this: each time a refresh token is used, the server issues a new one and invalidates the old one immediately. If a stolen refresh token is replayed after the legitimate client has already rotated past it, the server sees an invalidated token being reused. That's a strong sign of theft, and the server can revoke the whole token family instead of rejecting a single request.
Where you store the refresh token matters. In localStorage, any script running on the page can read it, so an XSS vulnerability anywhere in the app can steal it. An httpOnly, Secure cookie can't be read by JavaScript at all and is only sent by the browser to the intended origin. That makes it the safer default, at the cost of needing CSRF protection, because the browser attaches cookies automatically. Every storage option has a trade-off. The right one depends on whether your app is better placed to defend against XSS or against CSRF.
Checklist
- Decode to read what a token claims.
- Before trusting those claims for anything that matters, verify the token server-side with the real key, pinning the expected algorithm.
- Check the standard timestamp claims yourself instead of assuming your library does.
- Sign with a random secret that's long enough.
- If a token won't parse at all, read the malformed-token guide before suspecting the signature.
The tools and guides below cover each step.