JWT & Web Tokens Explained
What a JWT is, its three parts, standard claims, signing algorithms (HS256 vs RS256), verification pitfalls, revocation, and security best practices.
Last updated: 26 July 2026
A JWT (JSON Web Token, pronounced “jot”) is a compact, URL-safe way to represent claims — small statements like “this user is logged in and their ID is 42” — that can be verified and trusted because they are cryptographically signed. JWTs are the backbone of modern stateless authentication: an issuer signs a token, hands it to a client, and any service holding the right key can verify it without a database lookup. They are defined by RFC 7519.
The three parts
A JWT is three base64url-encoded segments joined by dots:
header.payload.signature
A real token looks like this (line-wrapped for readability):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQWRhIiwiaWF0IjoxNzAwMDAwMDAwfQ.
dQw4w9WgXcQ_signature_bytes_here
The critical thing to understand: the payload is not encrypted, only signed. Anyone can copy the middle segment and base64url-decode it to read every claim. The signature proves the token was not modified and came from a holder of the key — it does not hide the contents. Never put secrets, passwords, or sensitive personal data in a JWT.
Header
Base64url-decoding the first segment gives JSON describing how the token is signed:
{
"alg": "HS256",
"typ": "JWT"
}
alg— the signing algorithm (see below).typ— the token type, usuallyJWT.kid— an optional key ID, letting the verifier pick the right key when several are in rotation.
Payload
The second segment holds the claims — the actual data:
{
"iss": "https://auth.example.com",
"sub": "123456",
"aud": "https://api.example.com",
"exp": 1700003600,
"iat": 1700000000,
"name": "Ada Lovelace",
"role": "admin"
}
Claims fall into three groups: registered (standardised names like those above), public (collision-resistant custom names), and private (agreed between two parties, e.g. role).
Signature
The signature is computed over the encoded header and payload:
HMAC-SHA256(
base64url(header) + "." + base64url(payload),
secret
)
For asymmetric algorithms the HMAC is replaced by a private-key signature. This mechanism — signing a base64url header and payload — is standardised separately as JWS (RFC 7515).
Standard (registered) claims
| Claim | Name | Meaning |
|---|---|---|
iss |
Issuer | Who created and signed the token |
sub |
Subject | Who the token is about (usually the user ID) |
aud |
Audience | Who the token is intended for; verifiers must reject tokens not addressed to them |
exp |
Expiration | Reject the token at or after this Unix time |
iat |
Issued At | When the token was created |
nbf |
Not Before | Reject the token before this Unix time |
jti |
JWT ID | Unique identifier, useful for denylists and replay protection |
All time claims are NumericDate values — seconds since the Unix epoch.
Signing algorithms
The alg header names the algorithm. The two families you will meet in practice:
| Algorithm | Family | Key model | Who can verify | Typical use |
|---|---|---|---|---|
| HS256 / HS384 / HS512 | HMAC + SHA-2 | One shared secret | Anyone with the secret (who can also sign) | Single trusted backend issuing and verifying its own tokens |
| RS256 / RS384 / RS512 | RSA signature | Private key signs, public key verifies | Anyone with the public key (cannot sign) | OAuth/OIDC, multi-service or third-party verification |
| ES256 / ES384 / ES512 | ECDSA signature | Private key signs, public key verifies | Anyone with the public key | Same as RS256 but smaller keys/signatures |
- HMAC (HS)* uses a single symmetric secret for both signing and verifying. Simple and fast, but every verifier can also forge tokens, so it only suits a closed, single-party setup.
- Asymmetric (RS/ES)** splits the key: only the private key can sign, while the public key — which can be distributed freely, often via a JWKS endpoint — can only verify. This is what lets identity providers issue tokens that dozens of unrelated APIs trust. ES256 achieves comparable security to RS256 with much smaller keys and signatures.
How verification works
To trust a JWT, a verifier must:
- Split the token and base64url-decode the header and payload.
- Recompute the signature over
header.payloadusing the expected key and the algorithm you expect — not blindly the one named in the header. - Constant-time compare it with the token’s signature.
- Check the claims:
expis in the future,nbf/iatare not in the future,issandaudmatch what you expect.
Only if all of these pass is the token valid. A valid signature with a stale exp, or an aud meant for another service, must still be rejected.
Two dangerous attacks
alg: none. The JWS spec defines an “unsecured” mode wherealgisnoneand there is no signature. A naive library that honours the header will accept a completely unsigned, attacker-crafted token. Always rejectnoneand pin the algorithms you accept.- Algorithm confusion (RS256 → HS256). If a server verifies with RS256 but a library lets the token dictate the algorithm, an attacker can switch
algto HS256 and sign with the server’s public RSA key as if it were an HMAC secret. Since the public key is, by design, public, the forged token verifies. The fix is the same: the verifier decides the algorithm, never the token.
The golden rule: the verifier, not the incoming token, chooses the acceptable algorithm(s) and key.
Access vs refresh tokens
Because a signed JWT is valid until it expires and needs no server lookup, you face a tension between convenience and control. The standard answer is two tokens:
- Access token — a short-lived JWT (minutes to an hour) sent on every API request, usually as an HTTP
Authorization: Bearer <token>header per RFC 6750. Being stateless, it is fast to verify but hard to revoke. - Refresh token — a long-lived, opaque credential kept safely and stored server-side. When the access token expires, the client exchanges the refresh token for a new access token. Refresh tokens are tracked in a database, so revoking one (logout, password change, compromise) immediately cuts off future access.
The revocation problem
Statelessness is the whole appeal of JWTs — and also their biggest weakness. A leaked access token stays valid until exp no matter what. Mitigations, roughly in order of statefulness:
- Keep access-token lifetimes short so the window of misuse is small.
- Maintain a denylist of revoked
jtivalues (adds a lookup, reduces statelessness). - Store a token version per user and reject tokens whose version is stale.
- Rely on refresh-token revocation for real “log everyone out” control.
HMAC signatures & webhook verification
The same HMAC primitive behind HS256 secures webhooks. When GitHub, Stripe or Slack calls your endpoint, they compute an HMAC-SHA256 of the raw request body using a secret you both share, and send it in a header (e.g. X-Hub-Signature-256 for GitHub, Stripe-Signature for Stripe). Your server recomputes the HMAC over the exact raw bytes and compares:
- Sign/verify over the raw body, before any JSON parsing or reformatting.
- Use a constant-time comparison to avoid timing attacks.
- Where a timestamp is included (Stripe, Slack), reject old requests to prevent replay.
It is the same idea as a JWT signature — a shared secret proving authenticity and integrity — just applied to an incoming HTTP request instead of a token.
Best practices
- Short expiry. Minutes for access tokens; lean on refresh tokens for longevity.
- Verify everything. Signature and
exp/nbfandaudandiss. A good signature alone is not enough. - Pin the algorithm. Reject
alg: none; never let the token choose the algorithm or key. - Always use HTTPS/TLS. A bearer token is a password in transit — anyone who captures it can replay it.
- Store safely. Prefer HttpOnly, Secure, SameSite cookies over
localStoragefor sensitive browser apps; use the OS secure store on mobile. - Keep payloads lean and non-secret. Only claims a verifier needs; nothing sensitive.
- Rotate secrets/keys regularly and use
kidso you can roll keys without downtime. - Use a maintained library and keep it current — most JWT CVEs are verification bugs.
Standards & references
- RFC 7519 — JSON Web Token (JWT): structure and claims.
- RFC 7515 — JSON Web Signature (JWS): the signing mechanism.
- RFC 6750 — OAuth 2.0 Bearer Token usage (the
Authorization: Bearerheader). - RFC 7517 — JSON Web Key (JWK/JWKS): distributing public keys.
- jwt.io — decode and inspect JWTs, plus a list of libraries.
- MDN: SubtleCrypto — native signing and HMAC in the browser.
Try it
Decode a token or compute an HMAC signature entirely in your browser (nothing is uploaded) with the CodingSetu JWT and HMAC tools linked below.
Frequently asked questions
Is a JWT encrypted?
No — a standard signed JWT (JWS) is only signed, not encrypted. The header and payload are base64url-encoded, which anyone can decode and read. The signature only proves the token was not tampered with and was issued by someone holding the key. Never put secrets, passwords or sensitive personal data in a JWT payload. If you need confidentiality, use JWE (encrypted JWT) or keep the data server-side.
Where should I store a JWT?
For browser apps, an HttpOnly, Secure, SameSite cookie is generally safest because JavaScript cannot read it, which blocks token theft via XSS. localStorage and sessionStorage are readable by any script on the page, so a single XSS bug leaks the token — avoid them for sensitive apps. Native/mobile apps should use the platform secure store (Keychain / Keystore).
Can I revoke a JWT?
Not easily. A JWT is valid until it expires because verification is stateless — the server just checks the signature and claims, with no database lookup. To revoke early you must add state back: keep a short expiry (minutes), maintain a denylist of jti values, or track a token version per user. Refresh tokens make this practical: keep access tokens short-lived and revoke the refresh token.
HS256 or RS256 — which should I use?
Use HS256 (HMAC with a shared secret) when the same trusted party both issues and verifies tokens, e.g. a single monolithic backend. Use RS256 or ES256 (asymmetric) when tokens are issued by one service and verified by many, or by third parties — verifiers only need the public key and can never mint tokens. Most OAuth/OIDC providers use RS256 or ES256.
What is the nbf claim?
nbf ("not before") is a timestamp before which the token must be rejected, even if the signature is valid. It lets you issue a token that only becomes active in the future. Verifiers should check that the current time is at or after nbf, just as they check it is before exp.