DTTooleras

JWT Tokens Explained: How Authentication Works in Modern Web Apps

A thorough explanation of JSON Web Tokens — how they work, the structure of header/payload/signature, access vs refresh tokens, common security pitfalls, and implementation patterns.

DevToolsHub Team17 min read878 words

What is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe token format used for securely transmitting information between parties. JWTs are the backbone of authentication in most modern web applications and APIs.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It consists of three parts separated by dots: Header.Payload.Signature

JWT Structure

Header

The header typically contains two fields:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg — The signing algorithm (HS256, RS256, ES256, etc.)
  • typ — The token type (always "JWT")

This JSON is Base64URL-encoded to form the first part of the JWT.

Payload (Claims)

The payload contains claims — statements about the user and additional metadata:

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Registered claims (standardized):

  • iss — Issuer
  • sub — Subject (usually user ID)
  • aud — Audience
  • exp — Expiration time (Unix timestamp)
  • nbf — Not before
  • iat — Issued at
  • jti — JWT ID (unique identifier for the token)

Custom claims can be anything you need: role, permissions, email, etc.

Signature

The signature is created by combining the encoded header, encoded payload, and a secret:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

The signature ensures that:

  1. The token hasn't been tampered with
  2. The token was issued by a trusted party

How JWT Authentication Works

The Flow

  1. User logs in with credentials (email/password)
  2. Server validates credentials against the database
  3. Server creates a JWT with user info and signs it with a secret key
  4. Server sends the JWT back to the client
  5. Client stores the JWT (usually in memory or httpOnly cookie)
  6. Client sends the JWT with every subsequent request (Authorization header)
  7. Server verifies the JWT signature and extracts user info
  8. Server processes the request based on the user's identity and permissions
Client                          Server
  |                               |
  |-- POST /login {email, pass} ->|
  |                               |-- Validate credentials
  |                               |-- Create JWT
  |<- 200 {token: "eyJ..."} -----|
  |                               |
  |-- GET /api/data ------------->|
  |   Authorization: Bearer eyJ...|
  |                               |-- Verify JWT signature
  |                               |-- Extract user from payload
  |<- 200 {data: [...]} ---------|

Access Tokens vs Refresh Tokens

A common pattern uses two types of tokens:

Access Token:

  • Short-lived (5-15 minutes)
  • Contains user claims
  • Sent with every API request
  • If compromised, limited damage window

Refresh Token:

  • Long-lived (days to weeks)
  • Stored securely (httpOnly cookie)
  • Used only to get new access tokens
  • Can be revoked server-side
// Token refresh flow
async function fetchWithAuth(url, options = {}) {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${accessToken}`,
    },
  });

  if (response.status === 401) {
    // Access token expired, try refresh
    const refreshResponse = await fetch("/api/auth/refresh", {
      method: "POST",
      credentials: "include", // sends httpOnly cookie
    });

    if (refreshResponse.ok) {
      const { accessToken: newToken } = await refreshResponse.json();
      accessToken = newToken;

      // Retry original request
      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${newToken}`,
        },
      });
    }
  }

  return response;
}

Security Best Practices

1. Use Strong Signing Algorithms

  • HS256 — HMAC with SHA-256. Symmetric (same key for signing and verification). Good for simple setups.
  • RS256 — RSA with SHA-256. Asymmetric (private key signs, public key verifies). Better for distributed systems.
  • ES256 — ECDSA with SHA-256. Asymmetric, smaller keys than RSA. Best performance.

Never use alg: "none" — This disables signature verification entirely.

2. Set Short Expiration Times

const token = jwt.sign(payload, secret, {
  expiresIn: "15m", // 15 minutes for access tokens
});

3. Don't Store Sensitive Data in the Payload

The payload is Base64-encoded, NOT encrypted. Anyone can decode it:

// Anyone can read this!
atob("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0");
// {"sub":"1234567890","name":"John Doe"}

Never put passwords, credit card numbers, or other secrets in the JWT payload.

4. Store Tokens Securely

  • Best: httpOnly, Secure, SameSite cookies (immune to XSS)
  • Acceptable: In-memory variable (lost on page refresh)
  • Avoid: localStorage or sessionStorage (vulnerable to XSS)

5. Validate All Claims

Always verify:

  • Signature is valid
  • Token hasn't expired (exp)
  • Issuer is correct (iss)
  • Audience is correct (aud)

6. Implement Token Revocation

JWTs are stateless by design — you can't "invalidate" a JWT without additional infrastructure. Common approaches:

  • Short expiration + refresh tokens — Most common
  • Token blacklist — Store revoked token IDs in Redis
  • Token versioning — Increment a version number in the database; reject tokens with old versions

Common JWT Pitfalls

  1. Using JWT for sessions — JWTs are great for API authentication but add complexity for traditional web sessions. Consider server-side sessions for simple web apps.

  2. Storing too much data — Every claim increases token size, and the token is sent with every request. Keep payloads small.

  3. Not validating the algorithm — Always specify the expected algorithm when verifying. Don't let the token's header dictate the algorithm.

  4. Confusing encoding with encryption — Base64 is NOT encryption. JWTs are signed, not encrypted (unless you use JWE).

Decode and inspect your JWTs with our JWT Decoder tool.

jwtjson web tokenauthenticationjwt tutorialjwt securityaccess tokenrefresh token

Related articles

All articles

Practice with free tools

200+ free developer tools that run in your browser.

Browse all tools →