Structure of a JWT
A JWT has three parts, each base64url-encoded and separated by a dot:
- Header: the signing algorithm (
alg), token type and often a key ID (kid). - Payload: the claims, such as who the user is, who issued the token and when it expires.
-
Signature: computed over
header.payloadwith the issuer's key. If one character of the header or payload changes, verification fails.
Registered claims
| Claim | Name | Meaning |
|---|---|---|
iss | Issuer | Who issued the token. Check it matches your identity provider. |
sub | Subject | The user or entity the token represents. |
aud | Audience | Which API the token is for. Reject tokens meant for other services. |
exp | Expiration | Unix time (seconds) after which the token is invalid. |
nbf | Not before | Unix time before which the token is invalid. |
iat | Issued at | When the token was created. |
jti | JWT ID | Unique ID, useful for revocation lists and replay protection. |
Verifying JWTs in your code
Decoding is not verifying. In your backend, always verify the signature, pin the allowed algorithms, and check
exp, iss and aud:
Node.js (jose)
// npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://issuer.example.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://issuer.example.com/',
audience: 'my-api',
algorithms: ['RS256'], // never let the token choose
});
Python (PyJWT)
# pip install "pyjwt[crypto]"
import jwt
payload = jwt.decode(
token,
key=public_key_pem,
algorithms=["RS256"],
audience="my-api",
issuer="https://issuer.example.com/",
)
Command line
# Decode the payload without verifying (bash + jq)
echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
Common JWT security mistakes
-
Accepting
alg: noneor letting the token's header choose the algorithm. Always pass an explicit allow-list of algorithms. - Algorithm confusion: verifying an RS256 public key as if it were an HS256 secret lets attackers forge tokens. Bind each key to one algorithm.
- Weak HMAC secrets: HS256 secrets should be at least 256 bits of randomness, not a password.
- Storing tokens in localStorage: any XSS bug can steal them. Prefer HttpOnly, Secure cookies.
- Long lifetimes: JWTs can't easily be revoked, so keep access tokens short-lived.
Frequently asked questions
How do I decode a JWT?
Paste the token into the box above. A JWT has three base64url-encoded parts separated by dots: header, payload and signature. The tool decodes the header and payload to JSON, explains standard claims, and shows exp, iat and nbf as readable dates. You can also paste a whole "Authorization: Bearer …" value.
Is it safe to paste a JWT into this site?
Decoding and verification run entirely in your browser with the Web Crypto API. The token and keys are never sent anywhere, and there is no share-link feature for this tool. Still, a valid JWT is a credential: prefer expired or test tokens, and never paste private keys.
Can anyone read the data in a JWT?
Yes. A signed JWT (JWS) is encoded, not encrypted, so anyone with the token can read its claims. The signature only proves it hasn’t been modified. Never put secrets or sensitive personal data in a JWT payload. If the payload must be confidential, use an encrypted JWT (JWE).
What is the difference between HS256 and RS256?
HS256 uses one shared secret to both sign and verify, so every service that verifies tokens could also forge them. RS256, ES256 and PS256 sign with a private key and verify with a public key, so you can safely publish the public key, for example through a JWKS endpoint. Identity providers like Auth0, Okta, Cognito and Entra ID use RS256 by default.
Why does my token show as expired?
The exp claim is a Unix timestamp in seconds. Once the current time passes it, servers must reject the token. Access tokens are usually short-lived (5–60 minutes) and are renewed with a refresh token. Also check for clock skew between servers, which libraries usually tolerate with a small leeway.