JWT Authentication: How JSON Web Tokens Work

ยท 10 min read

What is a JSON Web Token?

A JSON Web Token (JWT) is a compact, URL-safe way to represent claims between two parties. JWTs are widely used for authentication and authorization in modern web applications. When a user logs in, the server generates a JWT and sends it to the client, which includes it in subsequent requests to prove identity.

Unlike session-based authentication where the server stores session data, JWTs are stateless โ€” all the information needed to verify the token is contained within the token itself. This makes JWTs ideal for distributed systems, microservices, and single-page applications.

You can inspect and decode any JWT using our JWT Decoder tool to see its contents without needing to write any code.

JWT Structure Explained

A JWT consists of three parts separated by dots (.): Header, Payload, and Signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header

The header typically contains two fields: the token type (typ) and the signing algorithm (alg):

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

This JSON is Base64Url-encoded to form the first part of the JWT. Common algorithms include HS256 (HMAC-SHA256), RS256 (RSA-SHA256), and ES256 (ECDSA-SHA256).

2. Payload

The payload contains claims โ€” statements about the user and additional metadata:

{
  "sub": "1234567890",
  "name": "John Doe",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1516239022,
  "exp": 1516242622
}

Standard claims include:

Important: The payload is Base64Url-encoded, not encrypted. Anyone can decode it, so never put sensitive data like passwords in the payload. Verify this yourself with our Base64 Encoder/Decoder.

3. Signature

The signature ensures the token hasn't been tampered with. It's created by combining the encoded header and payload with a secret key:

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

You can use our Hash Generator to understand how HMAC-SHA256 works. When the server receives a JWT, it recalculates the signature and compares it with the one in the token. If they don't match, the token has been modified and is rejected.

๐Ÿ” Decode & inspect JWT tokens instantly

JWT Decoder โ†’ Hash Generator โ†’ Base64 Encoder โ†’

How JWT Authentication Works

Here's the typical JWT authentication flow in a web application:

  1. User logs in โ€” sends credentials (username/password) to POST /api/auth/login
  2. Server verifies credentials โ€” checks against the database
  3. Server generates JWT โ€” creates a token with user claims and signs it with a secret key
  4. Client stores the token โ€” typically in localStorage, sessionStorage, or an HttpOnly cookie
  5. Client sends token with requests โ€” usually in the Authorization: Bearer <token> header
  6. Server verifies token โ€” checks the signature and expiration on each request
  7. Server responds โ€” grants or denies access based on the token claims
// Client-side: Sending JWT with fetch
const response = await fetch('/api/protected/data', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});

// Server verifies the token before processing the request

Access Tokens vs Refresh Tokens

A robust JWT implementation uses two types of tokens:

Feature Access Token Refresh Token
PurposeAuthorize API requestsGet new access tokens
LifetimeShort (15 min โ€“ 1 hour)Long (7 โ€“ 30 days)
StorageMemory or localStorageHttpOnly cookie
Sent withEvery API requestOnly to refresh endpoint
RevocableNot easily (stateless)Yes (stored in DB)

When the access token expires, the client uses the refresh token to obtain a new access token without requiring the user to log in again. This pattern provides both security (short-lived access tokens) and convenience (long sessions via refresh tokens).

Node.js Implementation

Here's a complete JWT authentication implementation using Express and the jsonwebtoken library:

Installation

npm install jsonwebtoken bcryptjs express

Generating Tokens

const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

// Generate token pair
function generateTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, email: user.email, role: user.role },
    ACCESS_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { sub: user.id },
    REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  return { accessToken, refreshToken };
}

// Login endpoint
app.post('/api/auth/login', async (req, res) => {
  const { email, password } = req.body;

  // Find user in database
  const user = await User.findOne({ email });
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Verify password
  const isValid = await bcrypt.compare(password, user.passwordHash);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Generate and return tokens
  const tokens = generateTokens(user);

  // Store refresh token in HttpOnly cookie
  res.cookie('refreshToken', tokens.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });

  res.json({ accessToken: tokens.accessToken });
});

Middleware for Protected Routes

// Auth middleware
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  try {
    const decoded = jwt.verify(token, ACCESS_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// Protected route
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ userId: req.user.sub, email: req.user.email });
});

Token Refresh Endpoint

app.post('/api/auth/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token required' });
  }

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    const newAccessToken = jwt.sign(
      { sub: decoded.sub, email: decoded.email, role: decoded.role },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (err) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }
});

Security Best Practices

Follow these guidelines to keep your JWT implementation secure:

Common JWT Vulnerabilities

Be aware of these common attack vectors:

1. Algorithm Confusion Attack

An attacker changes the algorithm in the header from RS256 to HS256, then signs the token with the public key (which is... public). Always validate the algorithm server-side:

// WRONG - accepts any algorithm
jwt.verify(token, publicKey);

// CORRECT - explicitly specify allowed algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

2. The "none" Algorithm

Some libraries accept "alg": "none", which means no signature verification. Always reject tokens with no algorithm:

// Ensure algorithm is explicitly set
jwt.verify(token, secret, { algorithms: ['HS256'] });

3. Token Leakage via URL

Never pass JWTs as URL query parameters. They end up in server logs, browser history, and referrer headers. Always use the Authorization header or cookies.

4. Insufficient Token Expiration

Long-lived access tokens increase the window of attack if a token is compromised. Keep access tokens short (15 min) and use refresh tokens for longevity.

Frequently Asked Questions

Is JWT the same as OAuth?

No. OAuth 2.0 is an authorization framework, while JWT is a token format. OAuth often uses JWTs as access tokens, but they are separate concepts. OAuth defines the flow for granting access; JWT defines how the token is structured and verified.

Should I store JWTs in localStorage or cookies?

For access tokens, memory (JavaScript variable) is safest. For refresh tokens, use HttpOnly, Secure, SameSite cookies. Avoid localStorage for sensitive tokens as it's vulnerable to XSS attacks.

Can I revoke a JWT?

JWTs are stateless by design, so you can't directly revoke them. Common strategies include: short expiration times, token blacklists (requires server-side storage), refresh token rotation, and changing the signing secret (invalidates ALL tokens).

What happens when a JWT expires?

The server rejects the token with a 401 status. The client should then use its refresh token to get a new access token. If the refresh token is also expired, the user must log in again.

Related Tools

JWT Decoder Hash Generator Base64 Encoder JSON Formatter HTTP Status Codes