If you've ever logged into a site with Google, called a modern API, or peeked at a browser's network tab and seen a long string starting with eyJ, you've met a JWT. They're everywhere — the quiet workhorse of how today's apps decide who you are. And almost everyone who uses them carries one slightly dangerous misconception, which we'll get to.
A JWT — JSON Web Token, pronounced "jot" — is a compact, signed token that carries a set of claims from one system to another. The usual job: a server hands you a JWT when you log in, your browser sends it back on every request, and the server checks the signature to confirm it's genuine — without looking anything up in a database. That last part is the whole appeal, and also the whole problem.
Three parts separated by dots
A JWT is three Base64url-encoded chunks joined by full stops:
eyJhbGc...header . eyJzdWI...payload . SflKxw...signature
Header, payload, signature. You can paste any token into our JWT decoder to pull it apart, or decode the individual pieces yourself with a Base64 decoder — and that you can decode them, trivially, is the single most important fact about JWTs. More on that in a moment.
The header
The first chunk is a small JSON object describing the token itself:
{ "alg": "HS256", "typ": "JWT" }
alg is the signing algorithm — here HS256, an HMAC with SHA-256. typ just says this is a JWT. It's short and boring, but the alg field turns out to be the source of the format's most famous attacks, so keep it in mind.
The payload: the claims
The middle chunk is where the actual information lives, as a set of claims. Some are standardised — the spec calls them registered claims:
iss— the issuer, who minted the token.sub— the subject, usually the user ID.aud— the audience, who the token is meant for.exp— the expiry time, after which the token must be rejected.iat— when it was issued;nbf— a "not before" time.jti— a unique token ID.
Alongside those you can add your own claims — a username, a role, a tenant ID, whatever your application needs.
Here's the misconception. Because a JWT looks like an opaque blob of random characters, people assume it's encrypted. It is not. Base64url is an encoding, not encryption — it has no key and hides nothing. Anyone who holds the token can read every claim in the payload in about two seconds. Drop a token into any Base64 decoder and the payload reads back as plain JSON. The practical rule that falls out of this: never put anything secret in a JWT — no passwords, no card numbers, nothing you wouldn't write on a postcard. The signature stops people changing the token; it does nothing to stop them reading it.
The signature: the part that matters
The third chunk is what makes the first two trustworthy. The issuer takes the encoded header and payload, runs them through the algorithm named in alg, and attaches the result. Change a single character in the header or payload and the signature no longer matches — the token is rejected.
There are two broad families of signing algorithm, and the difference echoes the one between symmetric and asymmetric cryptography you'll find in TLS certificates:
- HMAC (HS256) — a symmetric scheme. One shared secret both signs and verifies. Simple, but everyone who can verify can also forge, so the secret has to stay on the server.
- RSA or ECDSA (RS256, ES256) — asymmetric. A private key signs; a separate public key verifies. The issuer keeps the private key; anyone can be handed the public key to check signatures without being able to mint new tokens.
Under the hood, HS256 is just an HMAC over the token using a secret key — the same building block behind a lot of integrity checking, which you can experiment with using a hash generator. The strength of the whole scheme rests on that key: an HMAC secret shorter than about 32 characters can be cracked offline, and once an attacker has the secret they can forge any token they like.
What JWTs are actually for
The headline use is stateless authentication. In the old model, logging in created a session row in a database, and every request carried a session ID the server looked up. With JWTs, the token itself carries the claims, signed, so the server just verifies the signature and trusts what's inside — no lookup, no shared session store. That scales beautifully across many servers and is why JWTs underpin OAuth 2.0 and OpenID Connect, the standards behind nearly every "sign in with…" button. They also travel naturally as API bearer tokens, sent in an Authorization header on each request.
The catch: you can't easily take one back
Statelessness has a sharp edge. Because the server keeps no record of issued tokens, it has no straightforward way to revoke one before it expires. If a token is stolen, or a user logs out, or you ban an account, that signed token keeps working until its exp passes — there's nothing to delete. It's the same fundamental tension behind certificate revocation: a self-contained, signed credential is hard to cancel mid-life.
The standard answers are a matter of damage control. Keep access tokens short-lived — minutes, not days — and issue a longer-lived refresh token that can be revoked centrally to mint new ones. For higher-stakes systems, maintain a denylist of revoked token IDs (the jti claim), which quietly reintroduces a bit of the server-side state JWTs were meant to avoid. There's no free lunch; you're choosing where to keep the state.
Where do you keep a token in the browser?
Once issued, a token has to live somewhere on the client, and the two common choices each carry a risk:
- Local storage — easy to use, but readable by any JavaScript on the page. A single cross-site scripting (XSS) flaw lets an attacker's script read the token and walk off with the user's session.
- An HttpOnly cookie — invisible to JavaScript, which defeats the XSS theft above, but cookies are sent automatically and need
SameSiteand anti-CSRF measures to avoid cross-site request forgery.
For most web apps the safer default is an HttpOnly; Secure; SameSite cookie. Either way, because XSS is the route to a stolen token, the broader defences against it matter here — a solid Content Security Policy and the rest of your HTTP security headers are part of keeping tokens safe, not just the page.
The classic JWT attacks
Most JWT disasters come from the verification side getting something subtly wrong. The greatest hits:
- The
alg: nonetrick. Early in JWT's life, the spec allowed an "unsigned" token withalgset tonone. Some libraries happily accepted these, so an attacker could strip the signature, setnone, and forge any claims they wanted. The fix is to rejectnoneoutright. - Algorithm confusion. If a server expects an asymmetric
RS256token but can be tricked into treating the token asHS256, it may use the public key — which is, by design, public — as the HMAC secret. The attacker signs a forged token with that known public key and it verifies. The fix is to pin the expected algorithm rather than trusting the one in the header. - Weak secrets. A short or guessable HMAC secret can be brute-forced offline, after which every token is forgeable.
- Skipping claim checks. A valid signature isn't enough — the server still has to verify
exp,aud,iss, and friends. A token that's correctly signed but expired, or meant for a different audience, must still be rejected.
None of these are exotic; they show up in penetration tests constantly, fresh CVEs land every year, and JWT mistakes sit squarely inside the OWASP Top 10 category for authentication failures. The defensive summary is short: use a well-maintained library rather than rolling your own, pin the algorithm, use a strong key, and validate every claim that matters.
The one-line takeaway
A JWT is a signed, self-contained, fully readable statement of claims. The signature makes it tamper-evident; it does not make it private, and it does not make it easy to cancel. Treat the payload as public, keep the signing key secret and strong, verify the algorithm and the claims on every request, and a JWT is a clean way to carry identity across a stateless system. Want to see inside one? Paste any token into our JWT decoder and watch the three parts come apart.
Decode any JSON Web Token
Paste a JWT into our decoder to split it into header, payload, and signature, read every claim back as formatted JSON, and check the signing algorithm and expiry at a glance.
Open the JWT Decoder →