What Is a JWT? Structure Explained with Examples
A JWT — JSON Web Token — is a compact, URL-safe string that lets a server confirm who you are without keeping a session record for you. It carries its own claims (who you are, when it expires) and a signature that proves nobody tampered with it. The server just checks the signature, trusts the claims inside, and moves on — which is why JWTs power so many modern APIs, single sign-on flows, and mobile apps.
What exactly is a JWT?
At its core, a JWT is a way to pass verified information between two parties as a JSON object. When you log in to a website, the server checks your username and password, then hands you a token instead of remembering your session in a database. On every later request, your browser or app sends that token back — usually in an Authorization: Bearer <token> header — and the server verifies the signature to confirm the token is genuine. No database lookup, no shared session store, no sticky server affinity: any server holding the right key can verify any token, which is what makes JWTs a natural fit for distributed systems and microservices.
That statelessness is the whole point, and it comes with a trade-off worth understanding early: a server cannot easily revoke a single JWT before it expires, because it does not keep a list of issued tokens. The standard answer is short lifetimes plus refresh tokens, covered in the security section below. For now, remember the shape: one string, three parts, separated by two dots.
The three parts of a JWT
Every JWT looks like xxxxx.yyyyy.zzzzz. The first part is the header, the second is the payload, and the third is the signature. The header and payload are JSON objects encoded with Base64URL (a URL-safe variant of Base64 that avoids characters like +, /, and = padding). The signature is computed over the first two parts, so any change to the header or payload breaks it.
| Part | What it contains | Readable without a secret? |
|---|---|---|
| Header | Token type (typ) and signing algorithm (alg), e.g. HS256 | Yes — Base64URL only |
| Payload | Claims: who the token is for, expiry, issuer, custom data | Yes — Base64URL only |
| Signature | Cryptographic signature over header + payload | Verifiable only with the key |
The most common beginner mistake is treating Base64URL encoding as encryption. It is not. Encoding just repackages data so it travels safely in a URL or header; anyone can reverse it in seconds. Paste a token into the JWT decoder and you will see the header and payload in plain JSON at once. The security lives entirely in the signature: it is what stops an attacker from editing the payload (say, changing a user ID or an expiry date) and passing the result off as genuine.
A worked example: decoding a real token
Take this widely used example token (signed with HS256, as published in the JWT documentation):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Step 1 — split on the dots. You get three segments: the header segment, the payload segment, and the signature segment. Two dots, three parts — if a "token" has any other number of segments, it is not a JWT.
Step 2 — decode the header. Base64URL-decoding the first segment gives:
{"alg":"HS256","typ":"JWT"}
This says the token is a JWT signed with HMAC-SHA256. The verifier reads alg first, because it decides which verification method to use — and, as the security section explains, it must never blindly trust this field.
Step 3 — decode the payload. The second segment decodes to:
{"sub":"1234567890","name":"John Doe","iat":1516239022}
Here sub is the subject (the user ID), name is a custom claim, and iat is "issued at" — 1516239022 in Unix time, which is 18 January 2018, 01:30:22 UTC. Note what is missing: there is no expiry claim in this teaching example, which is exactly why real tokens should always carry one.
Step 4 — check the signature. For HS256, the signature is computed as:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
The verifier recomputes this with its own copy of the secret and compares the result with the token's third segment. Match, and the token is authentic and untouched; mismatch, and it is rejected outright. Try it yourself: paste the example into the free JWT decoder to see the decoded header and payload, then change one character in the payload segment and watch the signature fail — that failure is the entire security model working as designed.
Claims: the standard fields in a payload
Claims are the name-value pairs inside the payload. Some are registered in the JWT standard (RFC 7519) and have agreed meanings; others are custom claims your application defines. A verifier typically checks the registered claims first, because they carry the security semantics.
| Claim | Name | What it means |
|---|---|---|
iss | Issuer | Who created and signed the token |
sub | Subject | Who the token is about, usually a user ID |
aud | Audience | Which service the token is intended for |
exp | Expiration | Unix time after which the token must be rejected |
nbf | Not before | Unix time before which the token must be rejected |
iat | Issued at | Unix time when the token was created |
jti | JWT ID | Unique identifier, useful for one-time-use tokens |
Custom claims sit alongside these — things like a user role ("role":"admin"), a subscription tier, or a tenant ID. They are convenient because the server can authorize a request from the token alone, but they are also frozen at issuance: if an admin demotes a user, the old token still says "admin" until it expires. That is one more reason to keep lifetimes short and to re-check critical permissions in the database for sensitive actions, rather than trusting the token alone.
HS256 vs RS256: which signing algorithm?
The header's alg field names the signing algorithm, and the choice shapes your whole architecture. HS256 uses one shared secret for both signing and verifying. RS256 uses a private key to sign and a public key to verify. Neither is "more secure" in the abstract — they solve different trust problems.
| HS256 (symmetric) | RS256 (asymmetric) | |
|---|---|---|
| Keys | One shared secret, held by signer and verifiers | Private key signs; public key verifies |
| Best for | A single service, or services that fully trust each other | Many services, third-party verifiers, single sign-on |
| Risk if leaked | Anyone with the secret can forge tokens | Leaked public key is harmless; only the private key matters |
| Key rotation | All verifiers must get the new secret at once | Publish a new public key; old tokens keep verifying until expiry |
As a rule of thumb: if one application both issues and verifies its own tokens, HS256 with a long, random secret (at least 256 bits, stored outside the codebase) is simple and fine. If several independent services — or outside parties — need to verify tokens without being able to create them, RS256 is the right call, because you can hand out the public key freely. Never use a weak or guessable secret with HS256: a short password-like secret can be brute-forced offline, and once an attacker has it, every token is forgeable.
Where JWTs go wrong: security pitfalls
Most JWT incidents are not breaks of the cryptography — they are implementation mistakes. The recurring ones are worth memorizing. First, accepting the none algorithm: some old libraries would accept a token with alg: none and no signature at all. Any modern library rejects this, but if you hand-roll verification, it is the first trap. Second, algorithm confusion: an attacker takes an RS256 token, re-signs it with HS256 using the server's public key as the "secret," and the server verifies it as HS256. The fix is to pin the expected algorithm instead of trusting the header. Third, skipping claim checks: verifying the signature but ignoring exp, aud, and iss turns an expired or foreign token into a valid one. Fourth, putting secrets in the payload: the payload is readable by design, so anything in it is public to whoever holds the token. And fifth, no transport security: a token sent over plain HTTP can be intercepted and replayed, so JWTs belong on HTTPS only, full stop.
Revocation deserves a final word. Because tokens are stateless, "logging out" usually just means deleting the token on the client — a copy an attacker already stole keeps working until it expires. Short access-token lifetimes are the main mitigation; for cases where instant revocation is truly required (banning a user, a confirmed breach), keep a small denylist of token IDs checked on sensitive routes, or switch those routes to a database-backed session. JWTs are a tool, not a religion: use them where stateless verification pays off, and reach for something else where it does not.
Practical tips
- Always set
exp, and keep it short. Five to fifteen minutes for access tokens, with a refresh token for renewal, is a sane default for most apps. - Verify everything, not just the signature. Check
exp,aud, andisson every request, and pin the expectedalg— never let the token choose its own verification method. - Keep payloads small. The token travels on every request, so a bloated payload taxes every API call. Carry IDs and roles, not profiles or permission lists.
- Store tokens thoughtfully. In browsers, weigh localStorage (XSS exposure) against httpOnly cookies with SameSite and CSRF protection; there is no option with zero trade-offs, so pick deliberately.
- Rotate keys on a schedule, not just after an incident. For RS256, publish the new public key before switching signers so old tokens keep verifying; for HS256, plan a coordinated secret rollover.
- Debug with a decoder, not with guesswork. When a token fails, decode it first: the header and claims usually reveal the problem (wrong audience, expired, wrong issuer) before you ever touch the signature logic.
Try it yourself
Paste any token in to see its header and claims decoded instantly — everything runs locally in your browser, nothing is uploaded.
Frequently asked questions
Is a JWT encrypted?
No. A JWT is signed, not encrypted. Anyone who holds the token can read the header and payload after Base64URL decoding — no secret is needed for that part. The signature only proves the contents were not changed. Never put passwords, API keys, or personal data in a payload; if the contents must stay secret, use an encrypted token format such as JWE instead.
How long should a JWT live?
Keep access tokens short — 5 to 15 minutes is a common range — and pair them with a longer-lived refresh token that can request a new one. Short lifetimes limit the damage if a token leaks, because a stolen token stops working quickly. Long-lived access tokens are convenient, but they turn a single leak into a long window of unauthorized access.
What happens when a JWT expires?
The server rejects it, usually with a 401 Unauthorized response, and the client must get a fresh token. In a typical setup the client sends its refresh token to a token endpoint, receives a new access token, and retries the failed request. If the refresh token has also expired, the user has to log in again from scratch.
Can I decode a JWT without the secret?
Yes, decoding and verifying are two different jobs. Decoding only Base64URL-decodes the first two parts, which anyone can do — that is how decoders show you the header and claims. Verification is the part that needs the secret (for HS256) or the public key (for RS256): it recomputes the signature and checks it matches. A token that decodes fine can still be rejected if its signature does not check out.
Why does my JWT fail with an invalid signature error?
The usual causes are a wrong secret or key, a token that was edited after signing, or an algorithm mismatch — for example, verifying an RS256 token with an HS256 secret. Copy-paste damage is another frequent culprit: a missing character, a line break, or trailing whitespace changes the signature input. Re-copy the token exactly, confirm the algorithm in the header, and verify against the same key the issuer used to sign it.
Should I store a JWT in localStorage?
It is a trade-off, not a clear yes or no. localStorage is easy to use but readable by any JavaScript on the page, so a cross-site scripting flaw can steal the token. An httpOnly cookie is invisible to JavaScript but needs CSRF protection on top. For browser apps, the cookie route with SameSite and CSRF defenses is usually the safer default, combined with short token lifetimes.