What Is a JWT? JSON Web Tokens Explained
A plain guide to JSON Web Tokens: the header, payload, and signature, how stateless auth works, what the parts mean, and the pitfalls that bite teams.
Log in to almost any modern web application and somewhere behind the scenes a JSON Web Token is doing the work. It is the string that travels with your requests to prove you are still the person who logged in a few minutes ago. JWTs are everywhere because they are simple, portable, and let a server verify a claim without asking a database. Those same properties are also where teams get into trouble.
This guide takes a JWT apart, shows what each piece does, explains the stateless model it enables, and walks through the pitfalls that show up in real breaches.
The shape of a token
A JWT is one long string with two dots in it. It looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFsaWNlIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Those two dots split it into three parts: header.payload.signature. Each part is Base64URL-encoded, which is a URL-safe variant of Base64. Decoding the first two parts gives you plain JSON. This matters and it surprises people: the contents are encoded, not encrypted. Anyone who holds the token can read the header and payload.
The header
The header is a small JSON object that describes the token itself. Two fields matter most:
{ "alg": "HS256", "typ": "JWT" }typ states that this is a JWT. alg names the algorithm used to produce the signature, for example HS256 (HMAC with SHA-256) or RS256 (RSA signature with SHA-256). Hold on to the alg field. It is convenient and it is also the source of the most famous JWT attacks.
The payload
The payload carries the claims, the statements the token is making. Claims come in registered, public, and private varieties. The registered ones defined by the standard are the important ones to know:
| Claim | Meaning |
|---|---|
iss | Issuer: who created the token |
sub | Subject: who the token is about, often the user id |
aud | Audience: who the token is intended for |
exp | Expiry time: after this, reject the token |
nbf | Not before: do not accept it before this time |
iat | Issued at: when it was created |
jti | A unique id for the token |
Alongside these you can add your own claims, such as a role or a tenant id. Because the payload is only encoded, never place passwords, API keys, or anything else sensitive in it. Treat everything in a JWT payload as public.
The signature
The signature is what makes the whole thing trustworthy. It is computed over the encoded header and encoded payload together, using the algorithm named in the header and a key. In pseudocode:
signature = HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
Because the signature covers both the header and the payload, changing a single character in either one makes the signature no longer match. A server that verifies the signature will reject a tampered token. The security of a JWT rests entirely on the server actually performing that check with the right key. For the underlying keyed-hash mechanism, see What Is HMAC.
A standard signed JWT hides nothing. The payload is Base64URL, which anyone can decode in a browser console. The signature stops tampering, not reading. If you need the contents to be secret, that is a separate mechanism (JWE), not a plain JWT.
Signing: shared secret or key pair
There are two families of signing algorithm, and the difference decides who can create valid tokens.
Symmetric (HMAC, for example HS256) uses a single shared secret. Whoever holds that secret can both create and verify tokens. This is simple and fast, and it fits a single service that both issues and checks its own tokens. The risk is that the same key does both jobs, so anyone who can verify can also forge.
Asymmetric (for example RS256 or ES256) uses a key pair. The issuer signs with a private key, and any number of services verify with the matching public key. The public key cannot create tokens, only check them. This is the right model when one authorization server issues tokens that many independent services must trust, which is common in OpenID Connect. See OAuth 2.0 Explained for where these tokens fit in a full login flow.
Stateless authentication
The reason JWTs took over is the stateless session model. In the classic approach, a server stores session data and hands the browser a random session id; every request, the server looks that id up. With JWTs, the token itself carries the claims, signed, so the server can verify the request by checking the signature and the expiry, with no lookup.
That buys real advantages. It scales horizontally with ease, because any server instance holding the verification key can validate a request without shared session storage. It works cleanly across services and domains. It removes a database round trip from every request.
The cost is the flip side of the same coin. A signed token is valid until it expires, and the server is not consulting any store that could say "this user logged out" or "this account is suspended". Revoking a JWT before its expiry is the hard problem of the whole model.
The usual answer is a two-token pattern. Issue a short-lived access token, the JWT that rides along on API calls, and a longer-lived refresh token that the client exchanges for a new access token when the old one expires. The refresh token is stored and checked server-side, so you can revoke it: delete it, and the user cannot mint new access tokens, and the last short-lived one dies on its own within minutes. This gives you the scaling benefit of stateless access tokens while keeping a genuine off switch through the refresh path. The design decision, and it is a real one, is how short to make the access token: shorter means a smaller theft window but more refresh traffic.
Keep access-token lifetimes short, on the order of minutes, and pair them with longer-lived refresh tokens you can revoke server-side. That way a stolen access token has a small window, and you retain a real off switch through the refresh flow. Decide this before launch, not after an incident.
The pitfalls that cause breaches
JWT problems cluster around a handful of recurring mistakes.
Trusting the alg header. The token tells the server which algorithm to use, and naive verification believes it. Two classic attacks follow. The none algorithm, where an attacker sets alg to none, strips the signature, and a lax library accepts an unsigned token. And key confusion, where a server expecting RS256 is tricked into treating the public key as an HMAC secret by switching alg to HS256. The fix is to fix the accepted algorithm on the server and never derive it from the token.
Not verifying at all, or verifying weakly. Decoding a JWT and reading its claims without checking the signature accepts anything anyone sends. Always verify the signature, the issuer, the audience, and the expiry.
Long or missing expiry. A token with no exp, or a very long one, is a credential that lives forever if stolen. Set short lifetimes.
Secrets in the payload. Covered above, and still common. The payload is public.
Weak HMAC secrets. A short or guessable HS256 secret can be brute-forced offline, letting an attacker forge valid tokens. Use a long, random secret.
Storing the token unsafely in the browser. A JWT is only as safe as the place you keep it. Put an access token in localStorage and any cross-site scripting flaw on your page can read it and walk off with a valid credential. Keep it in a cookie marked HttpOnly, Secure, and SameSite, and script cannot read it, which shrinks the blast radius of an injection considerably. This is a browser-side decision that people often make by accident, so make it on purpose. A stolen token is a live session until it expires, which loops back to keeping lifetimes short and is a common path to session hijacking.
For a fuller treatment of these attacks, see JWT Attacks Explained. To see which JWT and token-handling library flaws are currently being exploited in the wild, our Exploit Intelligence dashboard tracks live vulnerabilities across authentication components so you can prioritise the ones under real pressure.
Using JWTs safely
The short version fits on one hand. Verify every token's signature with a server-fixed algorithm. Check exp, iss, and aud, along with the signature. Keep access tokens short and back them with revocable refresh tokens. Put nothing secret in the payload. Use a strong secret or a proper key pair. Do that, and the JWT does exactly the job it is good at: proving, cheaply and statelessly, that a request comes from someone who really logged in.
Decoding a token by hand
The best way to internalise how little a signed JWT hides is to take one apart. The example string near the top of this guide has three dot-separated parts. The first part decodes from Base64URL to the JSON header, which reads as an object with alg set to HS256 and typ set to JWT. The second part decodes to the payload, a JSON object with a subject claim and a name claim. Neither decode step requires any key or secret. Any browser console, any command-line tool, any small script can turn those first two segments back into readable JSON in an instant.
That is the whole point of the "encoded, not encrypted" warning made concrete. Base64URL is a reversible transport encoding whose only job is to make binary data safe to carry in a URL or header. It scrambles nothing. If you drop a customer's email, a role, or an internal identifier into the payload, you have effectively published it to anyone who ever holds the token, including the browser it sits in and any log that records it.
The third segment, the signature, is where a key finally enters. To verify an HS256 token, the server recomputes the HMAC over the exact bytes of the encoded header, a dot, and the encoded payload, using its secret, then compares the result to the signature segment. If a single character of the header or payload changed in transit, the recomputed value will not match and verification fails. The signature does not conceal the contents; it proves that the contents have not been altered since they were signed and that they were signed by a holder of the key. Reading and trusting are separate operations, and a JWT gives you strong assurance of the second while offering none of the first.
Walking through the decode also clarifies why the order of checks matters. A library that decodes the payload and returns its claims before, or instead of, verifying the signature hands you attacker-controlled data dressed up as trusted claims. The claims are only meaningful after the signature, the algorithm, the issuer, the audience, and the expiry have all been checked. Decoding is trivial; verifying is the security step.
The attacks, mechanism by mechanism
The famous JWT failures are worth understanding at the level of what the server is tricked into doing, because the pattern is the same each time: the server trusts something the attacker controls.
The none algorithm attack exploits a server that reads the algorithm from the token. An attacker takes a valid token, changes the header's alg to none, and removes the signature entirely. A library that honours the header's instruction sees none, concludes that no signature is required, and accepts the token as authentic. The attacker can now put any claims they like in the payload, including a different subject or an elevated role, and the server treats them as verified. The root cause is that the server let the token dictate its own verification rules.
The key confusion attack, sometimes called the algorithm-substitution attack, targets a server that verifies asymmetric tokens. Suppose the server issues RS256 tokens, signing with a private key and verifying with the matching public key, which is not a secret. An attacker crafts a token with the header's alg changed to HS256 and signs it using the public key as if it were an HMAC secret. A naive library, seeing HS256, calls its HMAC verification routine and reaches for the configured key, which for RSA verification is the public key. Because the attacker used that same public value to sign, the HMAC matches, and the forged token passes. The server's own public key becomes the forging secret. Both attacks share a fix: pin the accepted algorithm on the server side and never derive it from the token.
Weak HMAC secrets turn a well-designed scheme into a crackable one. If a service signs with HS256 using a short or dictionary-word secret, an attacker who captures one valid token can run an offline attack, guessing candidate secrets, recomputing the HMAC, and checking for a match, exactly the offline-guessing shape that plagues weak passwords. Once they recover the secret, they can forge any token they want. A long, random, high-entropy secret makes this infeasible.
Missing or unchecked claims are quieter but just as damaging. A server that verifies the signature but ignores exp accepts expired tokens forever. One that ignores aud may accept a token that was legitimately issued for a different service. One that ignores iss may accept a validly signed token from an issuer it never intended to trust. The signature proves integrity; the registered claims are what bind the token to a purpose, a lifetime, and an audience, and skipping them defeats their reason for existing.
Almost every serious JWT bug reduces to one sentence: the server trusted a value the attacker controlled. The alg header, the signature's presence, the claims inside the payload, all of these arrive from the client and mean nothing until the server has independently decided which algorithm and key it will accept and then checked the token against that decision. Fix the algorithm on the server, verify with the correct key, and validate the standard claims, and the entire family of forgery attacks closes at once.
JWTs compared with server-side sessions
Because JWTs are so often introduced as the modern replacement for sessions, it helps to see the two models side by side. Each makes a different trade, and the right choice depends on what you are building.
| Property | Server-side session | Stateless JWT |
|---|---|---|
| Where state lives | Server store, keyed by a random id | Inside the token, carried by the client |
| Revocation | Immediate: delete the server record | Hard: valid until it expires |
| Lookup per request | Database or cache read | Signature check, no lookup |
| Horizontal scaling | Needs shared session storage | Any node with the key can verify |
| Cross-service use | Awkward, tied to one store | Natural, especially with asymmetric keys |
| Blast radius of theft | Session can be killed at once | Token works until expiry |
The table explains why the two-token pattern exists. Pure server-side sessions give instant revocation and pay for it with a lookup on every request and shared storage. Pure long-lived JWTs give effortless scaling and pay for it with a revocation problem. Pairing a short-lived JWT access token with a revocable, server-checked refresh token takes the scaling benefit of the token for the common case, the per-request check, while keeping a genuine off switch on the refresh path. The access token stays stateless and fast; the refresh token stays stateful and revocable. This is the design most production systems converge on because it captures the useful half of each model.
Common mistakes
A handful of implementation mistakes account for most JWT trouble in practice, and they cluster around the same overconfidence in the token.
Decoding without verifying. Client-side or server-side, reading the payload for its claims without checking the signature accepts anything anyone sends. The decode is free and the verify is the security boundary, so treat any unverified claim as untrusted input.
Letting the token pick the algorithm. Passing the header's alg straight into the verification call is the doorway to the none and key-confusion attacks. Configure the one algorithm your server accepts and reject anything else before verification begins.
Storing access tokens in localStorage. Anything readable by JavaScript is readable by an injected script, so a single cross-site scripting flaw hands over a live credential. A cookie marked HttpOnly, Secure, and SameSite keeps the token out of script's reach and shrinks the damage an injection can do.
Putting sensitive data in the payload. The payload is public to anyone who holds the token. Roles and identifiers are fine; passwords, keys, and personal secrets are not.
Skipping expiry, audience, and issuer checks. A validated signature on a token you were never meant to accept still gets you compromised. Check that the token has not expired, was meant for your service, and came from an issuer you trust.
Treating logout as if it kills the token. Deleting a token from the browser stops that browser from sending it, but a copy an attacker already holds keeps working until it expires. Real invalidation runs through the revocable refresh token and short access-token lifetimes, which is why those two choices are load-bearing.
Frequently asked questions
Is a JWT encrypted? A standard signed JWT is not encrypted. The header and payload are Base64URL-encoded, which anyone can reverse to plain JSON without any key. The signature protects against tampering, not against reading. If you need the contents to be confidential, that is a separate mechanism (JWE, JSON Web Encryption), and it is a different construction from a plain signed token.
Where should I store a JWT in a browser?
Prefer a cookie marked HttpOnly, Secure, and SameSite, which keeps the token out of reach of page scripts and reduces what a cross-site scripting flaw can steal. Storing it in localStorage makes it readable by any script on the page, so a single injection can walk off with a valid credential. Whichever you choose, keep the access-token lifetime short.
Why is revoking a JWT hard? Because a stateless access token is trusted purely on its signature and expiry, with no server lookup that could mark it dead. The common answer is to keep access tokens short-lived and pair them with a longer-lived refresh token that is stored and checked server-side. Deleting the refresh token stops new access tokens from being minted, and the last short-lived one expires on its own within minutes.
What is the difference between HS256 and RS256?
HS256 is symmetric: one shared secret both creates and verifies tokens, which suits a single service that issues and checks its own tokens. RS256 is asymmetric: a private key signs and a separate public key verifies, so many independent services can verify tokens issued by one authorization server without being able to forge them. Choose RS256-style asymmetric signing when multiple parties must trust tokens from one issuer.
How long should an access token live? Short, typically on the order of minutes. A shorter lifetime shrinks the window in which a stolen token is useful, at the cost of more frequent refreshes. The refresh token carries the longer session and can be revoked server-side, so you get a small theft window on the access token and a real off switch on the refresh path.
Can I put a user's role or permissions in a JWT? Yes, that is a normal use of a private claim, and it is part of why JWTs enable stateless authorization. Keep in mind the payload is public, so a role is visible to anyone holding the token, and the values are only trustworthy after the signature and standard claims are verified. Sensitive data still does not belong in the payload.
What makes the alg header dangerous?
It lets the token suggest which algorithm the server should use to verify it, and a server that obeys that suggestion can be tricked. Setting alg to none invites the server to skip signature checking; switching it to HS256 on an RS256 system invites the key-confusion attack. Pin the algorithm on the server and ignore the token's claim about it.
Related guides
Sources & further reading
Related guides
- JWT Attacks: alg=none, Algorithm Confusion & Weak Secrets
How JSON Web Token attacks work: the alg=none trick, RS256-to-HS256 algorithm confusion, brute-forcing weak secrets, and how to defend.
- LDAP Injection: Filter Manipulation & Auth Bypass
How LDAP injection lets attackers rewrite directory search filters to bypass logins and read data, why it happens, and how to bind and query safely.
- OAuth Attacks: redirect_uri Manipulation, Codes & CSRF
How OAuth 2.0 attacks work: redirect_uri manipulation, stolen authorization codes, CSRF on the login flow, and how PKCE defends it.