Skip to content
JSON Tidy
Learn JSON / JSON Web Tokens

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 decoder

The 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:

ClaimNameIn the exampleWhat it means
issIssuer"https://auth.example.com"The login server that created and signed the token.
subSubject"user_8431"Who the token is about. Usually a stable user ID, not an email address.
audAudience"https://api.example.com"The service meant to accept it. Any other service should refuse it.
expExpiration time1790643600After this moment the token must be rejected.
nbfNot before(not used here)Before this moment the token must be rejected.
iatIssued at1790640000When the token was created.
jtiJWT 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:

Open the tampered token in the decoder

Shared secrets (HS256) vs key pairs (RS256, ES256)

HS256 / HS384 / HS512RS256, PS256, ES256, EdDSA
KeyOne shared secretA private key to sign, a public key to verify
Who can create tokensAnyone who can verify themOnly the private key holder
Good fitOne service that issues and checks its own tokensIdentity 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

  1. Maya signs in to an app. The app sends her to a login server, which checks her password or passkey.
  2. 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.
  3. When the app calls the API, it sends the access token in a header: Authorization: Bearer eyJhbGciOi…
  4. The API verifies the signature and claims, then uses sub and role to decide what Maya may do. It doesn’t need to call the login server or look up a session.
  5. 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:

  1. 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.
  2. Verify the signature with your own key. Get the key from your configuration or your issuer’s JWKS, using kid to pick one. Ignore the jwk, jku, and x5u headers, which would let the token supply its own key.
  3. Check the time claims. Reject tokens past exp or before nbf. Allow a small leeway, such as 30–60 seconds, for clock differences between machines.
  4. Check the issuer and audience. iss must be your login server, and aud must include your API.
  5. 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 secret or 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.