Glossary

What is a JWT (JSON Web Token)? How signed tokens work and how to use them safely

A JWT (JSON Web Token) is a token a server signs so that any tampering can be detected. How header/payload/signature work, and the dangers: skipping signature verification, allowing alg:none, and putting secrets in the payload.

Published 2026-06-12 Updated 2026-10-06 9 min read
On this page

A signed token a server hands you after login: that's a JWT. Here's how it works and the key points for using it safely (no attack steps).

How it works (three parts)

A JWT is header.payload.signature joined with .. The header and payload are just base64url-encoded (not encrypted); only the signature is made with a secret (or shared) key.

1. header

meta like the signing algorithm. anyone can read it

2. payload

claims like user id and expiry. anyone can read = no secrets

3. signature

made with a key. guarantees no tampering

A JWT has three parts. Header and payload are 'just readable'; the signature provides the authenticity guarantee.

The key point is that the contents are "just readable." So the payload is not a place for passwords or personal data. A JWT's value is that by verifying the signature, the server can confirm "I issued this token and it hasn't been altered."

Defenses for using it safely

1

Always verify the signature, and pin the alg (most important)

Verify the signature server-side every time. Pin the accepted signature algorithm (alg) to the value you expect, and reject alg:none (no signature). Don't trust the alg written in the token's header blindly.

The JWT best-practices RFC (RFC 8725) states that libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms, and that all cryptographic operations in the JWT must be validated, with the entire JWT rejected if any of them fail.

2

Block algorithm switching, not just alg:none

Abusing alg is not only about removing the signature. RFC 8725 describes how changing RS256 to HS256 makes some libraries try to validate the signature using HMAC-SHA256 with the RSA public key as the shared secret (algorithm confusion). The public key is, by definition, public — so if that works, an attacker can create a valid signature themselves.

The defense is the same: do not trust the header's alg; pin the algorithm the server expects. Decide the algorithm first and then verify, rather than picking a verification routine from the key material you were handed.

3

Validate who it is for, and until when

A valid signature only tells you the token is genuine. Whether it is for you and still current is a separate check.

RFC 7519 states that for aud, "if the principal processing the claim does not identify itself with a value in the 'aud' claim when this claim is present, then the JWT MUST be rejected", and that exp "identifies the expiration time on or after which the JWT MUST NOT be accepted for processing" (nbf works the same way at the other end). RFC 8725 adds that the application must validate that the keys used belong to the issuer. A token legitimately issued for a different service getting accepted by yours is what skipping these looks like.

4

Put no secrets in the payload

Assume anyone can decode the contents. Keep secrets — passwords, personal data, API keys — out of it. Include only claims that aren't catastrophic if seen, like identifiers and permissions.

5

Keep expiry short, and have a way to revoke

Make access tokens short-lived. An over-long expiry stretches the damage window if one is stolen. If you need revocation, pair a short-lived access token with a server-managed refresh/session.

6

Use a strong key and store it where it can't be stolen

Make the signing key strong, don't reuse it, and store it somewhere safe server-side. Protect the token itself too — keep it from being stolen via XSS, carry it in a cookie with safe attributes (→ beware theft via XSS).

This site's view: a JWT is not a session mechanism for every use

JWTs are often used to "hold login state," but they have a weakness: revocation is hard. If the signature is valid and unexpired, the server basically trusts it, so making a logout or invalidation take effect immediately is awkward.

Our stance is: don't use a JWT as a long-lived substitute for a session. Combine a short-lived access token with a server-side, revocable refresh/session, and you get both the JWT's upside (cheap verification) and easy revocation.

Caution: "it decoded" is not "it's valid"

Because the contents are readable, anyone can paste a JWT into a JWT decoder and inspect the header and payload. But decoding (reading the contents) and verifying (confirming authenticity) are entirely different. Being able to decode guarantees nothing about validity. What decides whether a token is genuine is always the server-side signature verification. Reading contents for debugging is handy — just don't mistake "I could read it" for "it's valid."

JWT vs. server-side sessions (cookies)

A JWT is just one way to hold login state, and it fits some cases better than a server-side session and others worse.

ItemServer-side session (cookie)JWT
Where state livesOn the server (DB or memory); the cookie holds only an IDInside the token; the server only verifies the signature
Logout / suspensionDeleting it on the server takes effect at onceAccepted until expiry unless you build revocation separately
Visibility of contentsOnly an ID, so nothing to readAnyone can decode and read it
Good fitAn ordinary web app on a single domainSeveral services or APIs verifying the same token

For a web app that lives on one domain, a server-side session is often enough. If you do choose JWTs, issuing a fresh token at login is the same principle as the defense against session fixation.

A real case: a library that didn't check the algorithm

Algorithm switching isn't just theory. The widely used Node.js JWT library jsonwebtoken, in versions 4.2.1 and earlier, didn't properly check the algorithm named in the token header, which allowed verification to be bypassed (CVE-2015-9235, fixed in 4.2.2). A token meant to be verified with a public key could be signed with a shared-key (HS-family) algorithm instead and still pass — exactly the algorithm confusion described above.

Two lessons:

  • Keep the library up to date. You can check your dependencies for known vulnerabilities by pasting a lockfile into the dependency vulnerability scanner
  • Don't rely on library defaults; state the accepted algorithm explicitly in your own code

How to check your own tokens

Paste a token your own service issued into the JWT decoder and look at the following. Decoding runs in your browser and the token is not sent to a server. Even so, for a live production token it's sensible to log out afterward so it's invalidated.

  1. Is the header's alg what you expect (not none)?
  2. Is there an exp, and is the lifetime from iat (issued-at) appropriate for the use?
  3. Are aud and iss present and pointing at your service?
  4. Does the payload contain anything you wouldn't want seen — email addresses, phone numbers, API keys?

Check server-side verification with tests in a development environment. A token with one signature character changed, a token with a different alg, an expired token, and a token meant for another service (wrong aud) should all be rejected — keep that as an automated test.

Sources (primary)

  • IETF RFC 8725, "JSON Web Token Best Current Practices" — datatracker.ietf.org (pinning algorithms, validating every cryptographic operation, algorithm confusion, keys belonging to the issuer)
  • IETF RFC 7519, "JSON Web Token (JWT)" — datatracker.ietf.org (the normative definitions of exp, nbf and aud)
  • GitHub Advisory Database, "Verification Bypass in jsonwebtoken" (CVE-2015-9235) — github.com/advisories (4.2.1 and earlier affected; fixed in 4.2.2)
  • OWASP HTML5 Security Cheat Sheet — cheatsheetseries.owasp.org (don't store session identifiers in localStorage)

FAQ

QIs the content of a JWT encrypted?
A

No. A standard JWT's header and payload are 'encoded' with base64url, not encrypted, so anyone holding the token can read the contents. So never put secrets like passwords or personal data in the payload. What a JWT protects is not 'confidentiality of contents' but 'that it hasn't been tampered with' (integrity, via the signature).

QWhat's the most dangerous JWT setting?
A

Not verifying the signature, or accepting 'alg:none' (no signature). Allowing that lets an attacker pass a forged token with freely rewritten contents. The defense is to always verify the signature server-side and pin the accepted signature algorithm (alg) to the value you expect.

QAre decoding and verifying the same thing?
A

No. Decoding just reads the contents and anyone can do it (you can check with a JWT decoder). Verifying is the server confirming, with a secret/public key, that the signature is valid, the token is unexpired, and the issuer/audience are correct. 'It decoded' does not mean 'it's a valid token' — server-side verification decides authenticity.

QShould I use JWTs or sessions (cookies)?
A

For an ordinary web app on one domain, a server-side session with a cookie is often enough, and logout or account suspension takes effect immediately. JWTs fit cases where several services or APIs verify the same token, or where you receive tokens from an external identity provider (OAuth/OpenID Connect). If you use JWTs, keep expiry short and pair them with something the server can revoke.

QCan I store a JWT in localStorage?
A

It's not recommended. localStorage is always readable by JavaScript, so a single XSS flaw lets an attacker steal the token. OWASP also advises against storing session identifiers in localStorage. In the browser, carrying the token in a cookie with HttpOnly, Secure and SameSite is safer; if you use a cookie, add CSRF protection as well.

QWhat is 'alg:none' in a JWT?
A

It's a JWT whose header sets alg (the signing algorithm) to none, meaning it has no signature. The spec allows it, but a token used for authentication such as login must never accept it; otherwise anyone can create a token with rewritten contents. Prevent it by pinning the accepted algorithm server-side to the value you expect (for example, RS256 only).