JWT Explained: How JSON Web Tokens Work and How to Use Them Securely

JSON Web Tokens (JWT) are everywhere in modern authentication — from single-page applications to mobile APIs and microservices. But they are also consistently misused. This guide explains exactly how JWTs work, what they protect against, where they fall short, and the critical security rules you must follow.

1. What Is a JSON Web Token?

A JWT (pronounced “jot”) is a compact, URL-safe token format defined in RFC 7519. It is used to transfer claims (pieces of information) between two parties in a way that can be cryptographically verified — but not necessarily encrypted.

The most common use is stateless authentication: after a user logs in, the server issues a JWT. The client stores the token (typically in memory or an HttpOnly cookie) and sends it with subsequent requests. The server validates the token without needing to look up a session in a database.

Key Insight: A JWT is not a session. It is a self-contained credential. The server trusts the token because it can verify the cryptographic signature — no database lookup needed.

2. JWT Structure: Header.Payload.Signature

A JWT consists of three Base64Url-encoded parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzExNDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header

The header declares the token type and the signing algorithm:

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

Payload (Claims)

The payload contains the claims — statements about the user and any additional metadata. RFC 7519 defines several registered claim names:

  • sub — Subject (typically the user ID)
  • iss — Issuer (who issued the token)
  • aud — Audience (who the token is intended for)
  • exp — Expiration time (Unix timestamp)
  • iat — Issued at (when the token was issued)
  • nbf — Not before (token is invalid before this time)
  • jti — JWT ID (unique identifier for the token)
{
  "sub": "1234567890",
  "name": "Alice",
  "role": "admin",
  "iat": 1711400000,
  "exp": 1711403600
}
Security Warning: The payload is Base64Url-encoded, not encrypted. Anyone who intercepts the token can decode and read its contents. Never store passwords, payment details, or other sensitive secrets in a JWT payload.

Signature

The signature ensures the token has not been tampered with. It is computed as:

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

If any part of the token is modified (even a single character), the signature will not match and the server must reject the token.

3. How JWT Authentication Works

The typical JWT authentication flow:

  1. Login: User submits credentials to POST /auth/login
  2. Issue token: Server verifies credentials, generates a JWT signed with a secret key, and returns it to the client
  3. Store token: Client stores the JWT (memory, cookie, or secure storage)
  4. Authenticated request: Client sends the JWT in the Authorization: Bearer <token> header
  5. Verify token: Server validates the signature, checks exp, and trusts the claims
  6. Process request: Server uses the claims (e.g. sub, role) without a database lookup
// Node.js example — issuing a JWT
const jwt = require('jsonwebtoken');

const token = jwt.sign(
  { sub: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '1h', issuer: 'api.myapp.com', audience: 'myapp.com' }
);

// Verifying a JWT
try {
  const payload = jwt.verify(token, process.env.JWT_SECRET, {
    issuer: 'api.myapp.com',
    audience: 'myapp.com'
  });
  console.log('User ID:', payload.sub);
} catch (err) {
  // Token is invalid, expired, or tampered
  res.status(401).json({ error: 'Unauthorized' });
}

4. Signing Algorithms: HS256 vs RS256 vs ES256

HS256 — HMAC-SHA256 (Symmetric)

Uses one shared secret key for both signing and verification. Fast and simple, but the same key must be available to every service that verifies tokens. If a service is compromised, it could also forge tokens.

Use when: A single application or monolith issues and verifies tokens.

RS256 — RSA-SHA256 (Asymmetric)

Uses a private key to sign and a public key to verify. The public key can be freely distributed to all services that need to verify tokens, but only the auth service holding the private key can issue new tokens.

Use when: Multiple microservices need to verify tokens but only one service should issue them.

ES256 — ECDSA-SHA256 (Asymmetric)

Similar to RS256 but uses Elliptic Curve cryptography. Produces much shorter signatures (64 bytes vs ~256 bytes for RS256) while providing equivalent security. Preferred for resource-constrained environments.

Recommendation: Use RS256 or ES256 for new production systems. The asymmetric key approach is more secure and scales better in microservice architectures.

5. Critical Security Considerations

5.1 Always Verify the Algorithm

The most infamous JWT vulnerability: the “none” algorithm attack. Some early libraries accepted {"alg":"none"} in the header, bypassing signature verification entirely.

// VULNERABLE — never do this
const payload = jwt.decode(token); // only decodes, does NOT verify

// SECURE — always verify with an explicit algorithm
const payload = jwt.verify(token, secret, { algorithms: ['HS256'] });

5.2 Always Validate exp, iss, and aud

A token with a valid signature but an expired exp must still be rejected. Always validate iss (issuer) and aud (audience) to prevent a token issued for one service from being used in another.

5.3 Use Short Expiry + Refresh Tokens

Long-lived JWTs are difficult to revoke if stolen. Use short-lived access tokens (15–60 minutes) paired with a refresh token stored in an HttpOnly cookie. The refresh token allows silently obtaining new access tokens without requiring re-login.

5.4 Store Tokens Safely

The safest option for web applications is an HttpOnly, Secure cookie — inaccessible to JavaScript - combined with SameSite=Strict to prevent CSRF. Avoid localStorage — it is accessible to any JavaScript on the page, including malicious injected scripts.

5.5 Never Put Sensitive Data in the Payload

The payload is encoded, not encrypted. Store only non-sensitive identifiers (user ID, roles). Never store passwords, credit card numbers, PII, or secrets in JWT claims.

5.6 Rotate Signing Keys

Periodically rotating signing keys limits the blast radius if a key is compromised. Implement key versioning with a kid (key ID) header parameter so clients can look up the correct public key.

6. JWT vs Server-Side Sessions

Aspect JWT (Stateless) Server Sessions (Stateful)
ScalabilityExcellent — no shared state neededRequires sticky sessions or shared session store (Redis)
RevocationDifficult — need a denylist or short expiryImmediate — delete the session record
Payload sizeSent with every request — keep smallOnly a session ID sent (small cookie)
Data freshnessStale until expiry — role changes not instantAlways fresh — data read from store per request
Best forAPIs, microservices, mobile clientsTraditional web apps with server-rendered pages

JWTs are not universally superior. For applications that need instant token revocation (financial systems, healthcare, admin panels), traditional sessions with a shared store are often a better choice despite the added infrastructure complexity.

7. Decoding JWTs in Practice

You can decode any JWT to inspect its contents using our JWT Decoder/Encoder tool. Remember: decoding is not the same as verifying. Always use a library with the proper secret key to verify signatures in production code.

// Quick base64url decode in the browser console
const [header, payload] = 'eyJ...token...'.split('.');
console.log(JSON.parse(atob(header.replace(/-/g,'+').replace(/_/g,'/'))));
console.log(JSON.parse(atob(payload.replace(/-/g,'+').replace(/_/g,'/'))));

Frequently Asked Questions

Are JWTs encrypted?

Not by default. A standard JWT (JWS) is only signed, not encrypted. Anyone who intercepts the token can Base64Url-decode and read the payload. JWE (JSON Web Encryption) provides actual encryption but is far less common. Never put sensitive secrets in a JWT payload.

What happens if a JWT is stolen?

The stolen token is valid until it expires — there is no server-side state to invalidate. Use short expiry windows (15–60 minutes), refresh tokens, and HttpOnly cookies to minimize the impact of token theft.

Should I store JWTs in localStorage or cookies?

HttpOnly cookies are safer. They are inaccessible to JavaScript, preventing XSS token theft. Also set Secure and SameSite=Strict. localStorage is convenient but exposes tokens to any JavaScript on the page.

What is the “none” algorithm vulnerability?

Some early JWT libraries accepted "alg": "none", bypassing signature verification entirely. Always explicitly specify allowed algorithms in your verifier and reject tokens with "none" as the algorithm.

Can I use JWT without a library?

You can decode the header and payload without a library. But never implement signature verification yourself — use a well-tested cryptography library. Subtle implementation errors in signature verification are a common security vulnerability.