JWT explained: structure, claims, and security pitfalls

A clear explanation of JSON Web Tokens, from the three parts of a token to standard claims, signing algorithms, safe verification, and the mistakes that cause real vulnerabilities.

What a JWT is

A JSON Web Token (JWT, often pronounced "jot") is a compact, URL-safe string that carries a set of claims, such as who a user is and when the token expires, together with a signature that lets the receiver check the token has not been altered. JWTs are defined in RFC 7519 and are widely used for API authentication, single sign-on, and passing identity between services.

The key idea is that the server does not need to look anything up to trust a valid token. If the signature checks out with the right key, the claims inside are exactly what the issuer wrote. That makes JWTs convenient for distributed systems, and it is also the source of most JWT mistakes.

The three parts of a token

A JWT is three Base64url-encoded parts separated by dots: a header, a payload, and a signature. Here is a well-known example token, split onto lines for readability:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9                                   <- header
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ  <- payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c                            <- signature

Decoding the first two parts gives plain JSON:

{ "alg": "HS256", "typ": "JWT" }

{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

The header names the signing algorithm. The payload holds the claims. The signature is computed over the encoded header and payload, so changing even one character in either part makes the signature invalid.

Base64url encoding is not encryption. Anyone who has a token can read its payload, so never put passwords, secrets, or sensitive personal data in a JWT.

Standard claims

RFC 7519 registers a handful of short claim names that most libraries understand. Time-based claims are Unix timestamps in seconds, not milliseconds.

ClaimNameMeaning
issIssuerWho created and signed the token
subSubjectWho the token is about, usually a user ID
audAudienceWhich service the token is intended for
expExpiration timeAfter this time the token must be rejected
nbfNot beforeBefore this time the token must be rejected
iatIssued atWhen the token was created
jtiJWT IDA unique ID, useful for detecting replay or revoking a token

Applications add their own claims too, such as roles, scopes, or a tenant ID. Keep custom claims small: tokens travel with every request, often in headers with size limits.

Signing algorithms

AlgorithmTypeKeys
HS256, HS384, HS512HMAC with SHA-2One shared secret signs and verifies
RS256RSA signaturePrivate key signs, public key verifies
ES256ECDSA signaturePrivate key signs, public key verifies; smaller keys and signatures

HMAC algorithms are simple and fast, but every service that verifies tokens must hold the same secret, and any of them could also mint new tokens. Asymmetric algorithms such as RS256 and ES256 let many services verify tokens with a public key while only the issuer holds the private key, which is why identity providers usually use them and publish their public keys at a JWKS endpoint.

If you use HMAC, the secret must be long and random. A short or guessable secret can be brute-forced offline from a single captured token.

Decoding versus verifying

Decoding a token just reads its contents, which is useful for debugging. In JavaScript, convert Base64url to Base64 and parse the JSON:

function decodeJwtPart(part) {
  const base64 = part.replace(/-/g, '+').replace(/_/g, '/');
  const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
  return JSON.parse(atob(padded));
}

const [header, payload] = token.split('.');
console.log(decodeJwtPart(header), decodeJwtPart(payload));

Verifying is what makes a token trustworthy: checking the signature with the expected key and algorithm, then checking the expiry, issuer, and audience. Always use a maintained library for this, and tell it exactly which algorithms to accept.

Node.js with the jsonwebtoken package
import jwt from 'jsonwebtoken';

const claims = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256'],
  issuer: 'https://auth.example.com',
  audience: 'orders-api',
});

Security pitfalls

  • Trusting decoded data without verifying. Reading claims from an unverified token lets anyone impersonate anyone.
  • **Accepting the none algorithm.** Unsigned tokens must be rejected. Pin the allowed algorithms instead of trusting the header.
  • Algorithm confusion. If a server that expects RS256 also accepts HS256, an attacker may sign a token using the public key as an HMAC secret. Pinning algorithms prevents this.
  • **Ignoring exp, aud, or iss.** A correctly signed token for a different service, or one that expired last month, should still be rejected.
  • Long-lived tokens. A stolen token is valid until it expires. Keep access tokens short-lived, often minutes, and use refresh tokens to obtain new ones.
  • No way to revoke. Because verification needs no database lookup, logging out does not invalidate an issued token. Short lifetimes, token IDs on a deny list, or rotating keys address this.

Where to store tokens in the browser

Tokens in localStorage are easy to use from JavaScript, which also means any cross-site scripting (XSS) bug can read and steal them. Tokens in HttpOnly cookies cannot be read by scripts, which protects against theft through XSS, but cookies are sent automatically, so you need cross-site request forgery (CSRF) protection, such as SameSite cookies and CSRF tokens.

For many web applications, an HttpOnly, Secure, SameSite cookie holding a short-lived token is the more robust default. Whatever you choose, preventing XSS in the first place matters more than the storage location.

When a JWT is the right tool

JWTs shine when several independent services need to trust the same identity without sharing a session database, and when tokens are short-lived. For a single web application with one backend, a traditional server-side session identified by a cookie is often simpler and easier to revoke. Choose JWTs for the problem they solve, not because they are common.

To inspect a token while debugging, paste it into a JWT inspector that runs in your browser, such as the one in UtiliStudio Developer Utilities. Use test tokens where possible, and treat real production tokens like passwords.

Try it: Developer UtilitiesEssential developer helpers including hash generators, base64 encoders, and regex testers.
Open Developer Utilities