TL;DR: A JWT (JSON Web Token) is a signed string in three dot-separated parts: header, payload, and signature. The payload carries claims about the user (ID, email, roles, expiry). The signature cryptographically proves the token was issued by your server and has not been modified. JWTs are not encrypted — the payload is readable by anyone. Never put passwords or sensitive data in a JWT.

What Is a JWT?

A JWT (pronounced "jot") is a standard format for transmitting information between parties as a signed JSON object. The key word is signed — it means you can verify the data has not been tampered with.

In practice, JWTs are used as authentication tokens. When a user logs in, your server creates a JWT containing the user's ID (and maybe their role or email), signs it with a secret key, and sends it to the client. The client sends this token back in every subsequent request. The server verifies the signature — if it checks out, the user is authenticated, without ever touching the database.

Real Scenario

You ask Claude: "Add login to my Express API." Claude generates a /auth/login endpoint that checks the username and password, then calls jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' }) and returns the token. Your frontend stores it and sends it in the Authorization: Bearer [token] header on every API request. Middleware on the server calls jwt.verify(token, process.env.JWT_SECRET) to check it.

The Three Parts of a JWT

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6ImNodWNrQGV4YW1wbGUuY29tIiwiaWF0IjoxNzQyMDgwMDAwLCJleHAiOjE3NDI2ODQ4MDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three parts, separated by dots:

1. Header

// Base64-decoded:
{ "alg": "HS256", "typ": "JWT" }

Specifies the signing algorithm (HS256 = HMAC-SHA256) and the token type.

2. Payload (Claims)

// Base64-decoded:
{
  "userId": "123",
  "email": "chuck@example.com",
  "iat": 1742080000,   // issued at (Unix timestamp)
  "exp": 1742684800    // expiry (Unix timestamp)
}

This is the data you put in the token. Standard claims: iat (issued at), exp (expiry), sub (subject, usually user ID), iss (issuer). Custom claims are anything else you add.

Critical: The payload is Base64-encoded, not encrypted. Anyone can decode it with atob() in a browser or at jwt.io. Do not put passwords, SSNs, payment info, or anything sensitive in the payload.

3. Signature

HMACSHA256(
  base64url(header) + "." + base64url(payload),
  your_secret_key
)

The server uses its secret key to create a hash of the header and payload. When the server receives a token later, it re-computes the hash and checks if it matches the signature. If someone modifies the payload (e.g., changes userId from 123 to 456), the signature will not match and the token is rejected.

Using JWTs in Node.js

import jwt from 'jsonwebtoken'

const SECRET = process.env.JWT_SECRET // must be a long random string

// Creating a token (after login)
const token = jwt.sign(
  { userId: user.id, email: user.email },
  SECRET,
  { expiresIn: '7d' }
)

// Verifying a token (in auth middleware)
try {
  const decoded = jwt.verify(token, SECRET)
  // decoded = { userId: '123', email: 'chuck@example.com', iat: ..., exp: ... }
  req.user = decoded
  next()
} catch (error) {
  if (error.name === 'TokenExpiredError') {
    return res.status(401).json({ error: 'Token expired' })
  }
  return res.status(401).json({ error: 'Invalid token' })
}

Where to Store JWTs in the Browser

  • HttpOnly cookies (recommended): JavaScript cannot read HttpOnly cookies, so XSS attacks cannot steal the token. The browser sends them automatically with requests to your domain. Use with SameSite=Strict or Lax to prevent CSRF attacks.
  • localStorage (common but risky): Easy to implement, but any JavaScript on your page (including injected XSS) can read it. Acceptable for low-risk apps; avoid for anything handling money or sensitive data.
  • sessionStorage: Like localStorage but cleared when the tab closes. Same XSS vulnerability.

AI typically generates localStorage storage because it is simpler. For production apps, push back and ask for HttpOnly cookies.

What AI Gets Wrong About JWTs

  • Weak secrets: AI sometimes generates JWT_SECRET=mysecret in .env examples. A JWT secret should be at minimum 32 random bytes. Generate one with openssl rand -base64 32.
  • Long expiry: AI defaults to 7-day or 30-day tokens. Prefer 15-minute access tokens + separate refresh tokens for production.
  • Sensitive data in payload: AI sometimes includes passwords or full user objects in the JWT payload. Include only what you need: user ID, role, and expiry.
  • Missing error handling: Expired and invalid tokens throw different errors. Handle both.
  • No https-only cookie flags: AI-generated cookie code often omits secure: true, allowing the cookie to be sent over HTTP.

What to Learn Next

Next Step

Paste any JWT from your app into jwt.io — you will immediately see the decoded header and payload. This makes JWT debugging trivial and proves in one click that the payload is readable without the secret key.

FAQ

A JWT (JSON Web Token) is a compact, signed string that carries information (called claims) about a user or session. It is digitally signed so the server can verify it has not been tampered with. JWTs are used as authentication tokens — sent with every API request to prove who the user is.

Header (algorithm and token type), payload (the claims — user ID, expiration, roles, etc.), and signature (a cryptographic hash of the header + payload using a secret key). Separated by dots. The signature proves the token has not been modified since it was issued.

Standard JWTs are signed but NOT encrypted. The payload is Base64-encoded, which is reversible — anyone can decode and read it without the secret key. Never put sensitive data (passwords, credit card numbers) in a JWT payload. The signature only proves it has not been tampered with.

HttpOnly cookies are more secure — JavaScript cannot access them, so XSS attacks cannot steal the token. localStorage is convenient but vulnerable to XSS. For production apps, use HttpOnly cookies with SameSite=Strict or Lax. Avoid localStorage for anything sensitive.

Standard JWTs cannot be invalidated before expiry. Common workarounds: use short expiry times (15 minutes) with a refresh token pattern, maintain a token blacklist in Redis, or store a token version in the database and validate it on each request.