JSON Web Tokens (JWTs) are the most common authentication format in modern web apps. If you have ever used an API, logged into a single-page app, or integrated with an OAuth provider, you have used JWTs even if you never saw them. They look like one long string of random-looking characters, but they are not random at all. This guide opens the box and explains exactly what is inside, how the signing actually works, when JWTs are the right choice, and the security mistakes that have leaked production tokens at major companies.
What is a JWT?
A JSON Web Token is a compact, URL-safe way to represent claims (statements) between two parties. The compact part means the token is small enough to fit in a URL, an HTTP header, or a cookie. The URL-safe part means it uses only characters that survive URL encoding without modification. The claims part is where the actual information lives: who the token is about, when it expires, what permissions the holder has, and any other data the issuer wants to include.
A typical JWT looks like this:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MDAwMDAwMDB9.QkjU3...
Three Base64URL-encoded chunks joined by dots. The chunks are not random; they are structured data you can decode and read right now. Try the JWT Decoder below and paste any token to see what is inside.
Free tool
Try the JWT Decoder now
Decode and inspect JSON Web Tokens
The three parts of a JWT
Part 1: the header
A small JSON object that describes how the token is signed. Decode the first chunk of any JWT and you usually see something like:
{"alg": "HS256", "typ": "JWT"}
alg tells you which algorithm produced the signature. The most common values are:
HS256: HMAC with SHA-256. Symmetric: the same secret signs and verifies. Fast, simple, requires the secret on every verifier.RS256: RSA signature with SHA-256. Asymmetric: a private key signs, a public key verifies. Used when verifiers should not have signing power (microservices, third-party APIs).ES256: ECDSA signature with SHA-256. Asymmetric like RS256 but with smaller keys and signatures. Increasingly popular in modern auth setups.
typ is almost always the literal string JWT. Some implementations omit it.
Part 2: the payload
Another JSON object containing the actual claims. Decoded, it might look like:
{"sub": "user_42", "iss": "https://auth.example.com", "exp": 1730000000, "role": "admin"}
Some claim names are reserved by the JWT spec (RFC 7519) and have specific meanings. Others are custom and mean whatever your application says they mean.
The seven standard claims you will see most often:
iss(issuer): who created the token.sub(subject): who the token is about, usually a user ID.aud(audience): who the token is intended for, usually an API identifier.exp(expiration time): Unix seconds; tokens past this are rejected.nbf(not before): token cannot be used before this time.iat(issued at): when the token was created.jti(JWT ID): unique identifier for this specific token.
Part 3: the signature
A cryptographic signature computed over the header and payload using the algorithm specified in the header. This is what makes JWTs trustworthy: anyone can read the payload, but only someone with the secret key can produce a valid signature.
For HS256, the signature is computed roughly like this:
signature = HMAC_SHA256(base64url(header) + "." + base64url(payload), secret)
The verifier recomputes this and checks that it matches. If anyone tampers with the header or payload, the recomputed signature will not match and the token is rejected.
Encoded, not encrypted
How JWTs are used in practice
The standard auth flow
- User logs in with email and password.
- Server verifies credentials and issues a JWT signed with a secret.
- Client stores the JWT (in memory, localStorage, or an HttpOnly cookie).
- Client sends the JWT in the Authorization header on every API request: Authorization: Bearer <jwt>.
- Server verifies the signature on every request, decodes the claims, and trusts the user identity inside.
Why this is popular
- Stateless: no session lookup. Just verify the signature.
- Cross-service: any service with the verification key can authenticate.
- Self-contained: the user identity, role, and permissions are inside the token itself.
- Mobile-friendly: works without cookies, perfect for native apps and SPAs.
The trade-off: revocation is hard
Because verification is stateless, there is no central place to delete a JWT. Once issued, it is valid until its exp claim, even if the user logs out, changes their password, or you delete their account. Mitigations:
- Use short expiration times (5 to 15 minutes for access tokens).
- Pair with longer-lived refresh tokens that can be revoked server-side.
- Maintain a token blacklist in Redis or similar (gives back some of the statelessness benefit).
- Use a token version claim that increments on logout, then check it server-side.
JWT vs session cookie
These are the two main authentication patterns. Neither is strictly better; the right choice depends on your architecture.
| JWT | Session cookie | |
|---|---|---|
| State location | In the token | In your database |
| Verification cost | CPU (signature check) | DB lookup |
| Revocation | Hard (token valid until exp) | Easy (delete row) |
| Cross-service auth | Trivial | Requires shared session store |
| Size | Larger (1 to 4 KB) | Small (just an ID) |
| Best for | APIs, microservices, mobile, SPAs | Traditional server-rendered web apps |
Signing keys: symmetric vs asymmetric
HS256 (symmetric)
Same secret signs and verifies. Simple, fast, and fine when the same service that issues tokens also verifies them. Risky when many services need to verify, because every verifier needs the secret, and a leak from any of them lets an attacker forge tokens.
RS256 / ES256 (asymmetric)
A private key signs (kept secret on the auth server). A public key verifies (can be shared widely with no risk). This is the right choice for microservices, third-party APIs, and any setup where you do not want every verifier to have the ability to forge tokens. The trade-off is slightly larger keys and slower signature operations.
Real-world JWT security mistakes
The alg none attack
Some older JWT libraries accepted tokens with alg: none, meaning “no signature required”. Attackers exploited this by stripping the signature and changing alg to none, then signing whatever claims they wanted. Most modern libraries reject this by default, but always specify the expected algorithm explicitly when verifying.
Algorithm confusion
Server expects RS256 (asymmetric) but is tricked into verifying an HS256 (symmetric) token using the public key as the symmetric secret. Public keys are not secrets; they are often published. Attacker forges tokens by signing with the public key. Mitigation: hard-pin the expected algorithm.
Storing JWTs in localStorage
localStorage is readable by any JavaScript on the page. If your site has an XSS vulnerability anywhere (third-party script, library breach, user content), the attacker can steal the JWT and impersonate the user. The common alternative is HttpOnly cookies, which JavaScript cannot read but require CSRF protection. There is no perfect answer; pick the trade-off you can defend.
Long expiration times
Issuing a JWT with a 30-day expiration may seem convenient, but it means a stolen token is valid for 30 days. Use short access tokens (5 to 15 minutes) and refresh tokens for longer sessions. This way, a leak via XSS or a compromised device limits damage.
Putting sensitive data in claims
JWT payloads are not encrypted. Putting raw passwords, API keys, social security numbers, or detailed PII in claims is a leak waiting to happen. Use minimal identifiers and look up real data server-side when needed.
Audit your JWTs
When to use JWTs
Use JWTs when
- You have an API that needs to authenticate requests without a session lookup.
- Multiple services need to verify identity without a shared session store.
- You are building a single-page app or mobile app that calls your backend.
- You are integrating with OAuth providers (Google, Auth0, Cognito) that issue JWTs natively.
Use server sessions when
- Your app is a traditional server-rendered website with login pages.
- You need easy revocation (kick a user out instantly).
- Your sessions hold lots of state you do not want to put in a token.
- You do not have a strong reason to be stateless.
Decoding and inspecting JWTs
When debugging auth issues, the first step is almost always to decode the JWT and see what is actually in it. Common causes of 401 errors: wrong audience claim, expired token, missing required scope, signed with the wrong key.
Decoding a JWT is simple in any language. Here is the algorithm:
- Split the token on dots into three parts.
- Convert each part from Base64URL to Base64 (replace - with +, _ with /, add padding =).
- Base64-decode each part.
- JSON-parse the first two parts (header and payload).
Verifying the signature is harder because it requires the secret or public key. For just reading the claims, decoding is enough.
Do not paste production tokens into random websites
Free tool
Decode any JWT privately in your browser
Decode and inspect JSON Web Tokens
Common questions
Are JWTs the same as OAuth tokens?
Not exactly. OAuth is an authorization framework. JWT is a token format. OAuth flows often use JWTs as the token format, but OAuth can use opaque tokens too. When someone says “OAuth access token”, it is often a JWT but not always.
Can I use the same JWT across different domains?
Yes, the token itself is portable. The challenge is sending it: cookies are scoped to a domain, but JWTs in the Authorization header travel cross-origin without issue (subject to CORS).
What size should a JWT be?
Aim for under 4 KB. Some web servers reject larger headers by default. Smaller is better for performance since the token rides on every request. Avoid stuffing every possible claim into one token.
How do I rotate signing keys?
Standard pattern: include a kid (key ID) claim in the header. The verifier looks up the matching public key from a JWKS endpoint. To rotate, publish the new key, gradually migrate signing to it, and remove the old key after all tokens signed with it have expired.
Summary
A JWT is three parts joined by dots: a header that says how the token is signed, a payload that holds the claims, and a signature that proves the token has not been tampered with. They are widely used because they enable stateless authentication across services. They are also widely misused because the format hides several real security pitfalls. Pick short expirations, prefer asymmetric algorithms when multiple services verify, never put secrets in the payload, and always pin the expected algorithm. Decode often, especially when debugging.