What is a JWT? How JSON Web Tokens work
A JSON Web Token (JWT, often pronounced “jot”) is a short string that carries signed JSON data between two systems. A login server typically creates one when a user signs in, and the app sends it with each request to an API. The data says who the user is, and the signature lets the API confirm the login server issued it, without calling the login server or checking a database.
The format is defined in RFC 7519. Here’s an example token. The rest of this page uses it to explain each part:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyXzg0MzEiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsIm5hbWUiOiJNYXlhIENoZW4iLCJyb2xlIjoiZWRpdG9yIiwiaWF0IjoxNzkwNjQwMDAwLCJleHAiOjE3OTA2NDM2MDB9.0ze5t8VduYPeB_bCfnpplpI9-UOTs9Z95lCH8D6QQho
Open this token in the JWT decoderThe three parts of a JWT
The dots split the token into three sections: a header, a payload, and a signature. The first two are JSON, encoded with Base64URL, a variant of Base64 that uses - and _ so the result is safe in URLs and HTTP headers. Decode the header and you get:
{
"alg": "HS256",
"typ": "JWT"
}alg names the signing algorithm, here HMAC with SHA-256, and typ says this is a JWT. Headers sometimes carry a kid (key ID) too, which tells the receiver which of the issuer’s keys to check against. The payload decodes to:
{
"iss": "https://auth.example.com",
"sub": "user_8431",
"aud": "https://api.example.com",
"name": "Maya Chen",
"role": "editor",
"iat": 1790640000,
"exp": 1790643600
}Each field in the payload is called a claim. The third part is the signature, a 32-byte HMAC value calculated from the first two parts. Signatures are covered below.
Base64URL doesn’t hide anything. Anyone with a copy of this token can decode it and read Maya’s name and role without a key, so passwords, API keys, and other secrets shouldn’t go in a payload. For more on the JSON itself, see the JSON format guide.
JWT claims
RFC 7519 defines seven registered claims. All of them are optional, but most tokens include several, and JWT libraries can check them for you:
| Claim | Name | In the example | What it means |
|---|---|---|---|
iss | Issuer | "https://auth.example.com" | The login server that created and signed the token. |
sub | Subject | "user_8431" | Who the token is about. Usually a stable user ID, not an email address. |
aud | Audience | "https://api.example.com" | The service meant to accept it. Any other service should refuse it. |
exp | Expiration time | 1790643600 | After this moment the token must be rejected. |
nbf | Not before | (not used here) | Before this moment the token must be rejected. |
iat | Issued at | 1790640000 | When the token was created. |
jti | JWT ID | (not used here) | A unique ID, useful for spotting replays or revoking one token. |
Times are whole seconds since January 1, 1970 UTC, not milliseconds. The example was issued at 1790640000 (midnight UTC on September 29, 2026) and expires at 1790643600, one hour later, so it has expired. Its signature still verifies, because expiry is checked separately.
name and role are custom claims agreed between the issuer and the API. You can use any names for custom claims, as long as they don’t reuse a registered name. Names that should mean the same thing everywhere, such as email, are listed in the IANA JWT claims registry. It’s worth keeping the payload small, since the token is sent with every request and some servers reject headers larger than about 8 KB.
How JWT signatures work
The issuer joins the encoded header and payload with a dot and signs that exact string. For HS256 the calculation is:
signature = HMAC-SHA256(
secret,
base64url(header) + "." + base64url(payload)
)The example was signed with the secret jsontidy-example-secret-not-for-production-use. It’s published here so you can verify the example yourself; a real secret is kept on the server. To check a token, the API repeats the calculation and compares the result with the signature. Changing any character in the header or payload produces a different result.
The link below opens a copy of the example with "role": "admin" instead of "editor" and the original signature. The decoder shows the edited payload, and the signature check fails:
Shared secrets (HS256) vs key pairs (RS256, ES256)
| HS256 / HS384 / HS512 | RS256, PS256, ES256, EdDSA | |
|---|---|---|
| Key | One shared secret | A private key to sign, a public key to verify |
| Who can create tokens | Anyone who can verify them | Only the private key holder |
| Good fit | One service that issues and checks its own tokens | Identity providers, and tokens checked by several services |
With HS256, any service that can verify tokens holds the same secret, so it could also create them. With a key pair, services that verify tokens only need the public key, which the issuer can publish as a JSON Web Key Set (JWKS). Auth0, Google, Microsoft, and Amazon Cognito all sign tokens with RS256 by default. If you do use HS256, make the secret long and random: at least 32 bytes for HS256, per RFC 7518. A short or guessable secret can be cracked offline from a single token.
How JWTs are used when you sign in
- Maya signs in to an app. The app sends her to a login server, which checks her password or passkey.
- The login server issues tokens. With OpenID Connect, that usually means an ID token for the app, saying who signed in, and an access token for the API.
- When the app calls the API, it sends the access token in a header:
Authorization: Bearer eyJhbGciOi… - The API verifies the signature and claims, then uses
subandroleto decide what Maya may do. It doesn’t need to call the login server or look up a session. - When the access token expires, the app uses a longer-lived refresh token to get a new one, without asking Maya to sign in again.
The ID token is meant for the app, so an API should accept only access tokens. Because the API in step 4 checks tokens without contacting the login server, it can’t easily cancel a token before it expires. This is the main reason access tokens are short-lived.
How to verify a JWT on the server
Before an API uses the claims in a token, it should check the following, in this order:
- Set the allowed algorithm. Accept only the algorithm you expect, not the one named in the token’s header. Libraries that let the token choose have had serious bugs. Tokens with
"alg": "none"were accepted with no signature at all. Some libraries also verified an HS256 token using the server’s RSA public key as the HMAC secret, which let anyone who knew the public key create valid tokens. - Verify the signature with your own key. Get the key from your configuration or your issuer’s JWKS, using
kidto pick one. Ignore thejwk,jku, andx5uheaders, which would let the token supply its own key. - Check the time claims. Reject tokens past
expor beforenbf. Allow a small leeway, such as 30–60 seconds, for clock differences between machines. - Check the issuer and audience.
issmust be your login server, andaudmust include your API. - Authorize the request using the claims.
JWT libraries handle all of these checks. Pass the algorithm, issuer, and audience explicitly rather than relying on defaults. In Node.js, with jose:
import { jwtVerify, createRemoteJWKSet } from "jose";
const keys = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));
const { payload } = await jwtVerify(token, keys, {
algorithms: ["RS256"],
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});The equivalent in Python uses PyJWT. With the example token, this raises ExpiredSignatureError: the signature is valid, but the token expired on September 29, 2026.
import jwt # pip install PyJWT
claims = jwt.decode(
token,
"jsontidy-example-secret-not-for-production-use",
algorithms=["HS256"],
issuer="https://auth.example.com",
audience="https://api.example.com",
)Common JWT mistakes
- Using claims without verifying the signature. Anyone can create a token containing any claims. The signature is what shows it came from your issuer.
- Putting secrets in the payload. Anyone with a copy of the token can read it.
- Tokens that never expire. Without
exp, a leaked token works until the signing key is rotated. - Timestamps in milliseconds. JavaScript’s
Date.now()returns milliseconds. Divide by 1000, or a one-hour token becomes valid for tens of thousands of years. - Weak HS256 secrets. Secrets such as
secretor a project name can be guessed in seconds. Generate at least 32 random bytes. - Pasting live tokens into tickets and chat. Anyone who has a valid token can use it. Share an expired token or a copy without its signature instead. The JWT decoder’s share link leaves the signature out by default.
Related standards: JWS, JWE, JWK, and JWA
These specifications are often mentioned alongside JWT:
- JWS (RFC 7515)
- A signed token: the three-part header.payload.signature format. Almost every JWT you meet is a JWS.
- JWE (RFC 7516)
- An encrypted token with five parts. Only the intended recipient can read the payload.
- JWK (RFC 7517)
- A key written as JSON. A JWK Set, or JWKS, is a list of them, which is how issuers publish public keys.
- JWA (RFC 7518)
- The list of algorithm names, such as HS256, RS256, and ES256.
Frequently asked questions
Is a JWT encrypted?
Usually not. A normal JWT is signed, which stops anyone changing it, but its header and payload are only Base64URL-encoded, so anyone holding the token can read them. Encrypted tokens exist (JWE, with five parts instead of three), but they’re much less common.
Can you decode a JWT without the secret?
Yes. Decoding needs no key at all, which is why a JWT decoder works on any token. You need the secret or public key only to verify the signature.
How long should a JWT last?
Access tokens commonly last between 5 and 60 minutes. Keep them short, because a stolen token works until it expires. Apps then use a longer-lived refresh token, which is checked against the server on each use, to get a new access token.
How do I invalidate or log out a JWT?
If your API only checks the signature and claims, a token stays valid until it expires. Common approaches are to keep access tokens short-lived and revoke the refresh token on logout, or to keep a list of revoked jti values that the API checks on each request.
Where should a web app store a JWT?
An HttpOnly, Secure cookie keeps the token out of reach of JavaScript, which limits the damage from cross-site scripting, but you then need CSRF protection. localStorage is simpler, but any script running on the page can read it. Many teams keep the access token in memory and the refresh token in an HttpOnly cookie.
Is a JWT the same as OAuth?
No. OAuth 2.0 is a protocol for granting access, and OpenID Connect adds sign-in on top of it. JWT is a token format. OpenID Connect ID tokens are always JWTs, and many OAuth servers issue JWT access tokens, but OAuth can use other token formats too.
Why does every JWT start with eyJ?
The header is a JSON object, so it starts with {". Base64URL-encoding those two characters always produces eyJ, and the payload usually starts the same way.