JSON Web Tokens Explained

Understanding the backbone of modern web authentication

What are JSON Web Tokens?

JSON Web Tokens (JWTs) are compact, self-contained tokens that allow secure transmission of information between parties as a JSON object. This information can be verified and trusted because it is digitally signed using a secret (with HMAC algorithm) or a public/private key pair (using RSA or ECDSA).

Real-World Analogy

Think of a JWT as a tamper-proof ID card. When you present your ID at a secure building:

  • The security desk (server) issues you an ID card (JWT)
  • The ID contains your photo and credentials (payload)
  • It has special holographic seals that are difficult to forge (signature)
  • Anyone in the building can look at your ID to verify you're authorized to be there
  • If someone tries to alter the ID, the holographic seals would show evidence of tampering

Why Use JWTs?

JWTs solve several challenges in modern web applications:

Practical Application

Some common real-world uses of JWTs include:

  • Authentication: After login, each subsequent request includes the JWT
  • Information Exchange: Securely transmitting information between parties
  • Single Sign-On (SSO): One token works across multiple services (like Google accounts)

JWT Structure

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

xxxxx.yyyyy.zzzzz
graph TD JWT[JWT Token] --> Header[Header: Algorithm & Token Type] JWT --> Payload[Payload: Claims/Data] JWT --> Signature[Signature: Verification] Header --> Base64Header[Base64Url Encoded] Payload --> Base64Payload[Base64Url Encoded] Base64Header --- Dot1[.] Dot1 --- Base64Payload Base64Payload --- Dot2[.] Dot2 --- Signature

Header

The header typically consists of two parts: the type of token (JWT) and the signing algorithm being used (like HMAC SHA256 or RSA).

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

Payload

The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.

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

Common Payload Claims

  • iss (Issuer): Who issued the token
  • sub (Subject): Who the token refers to
  • aud (Audience): Who the token is intended for
  • exp (Expiration Time): When the token expires
  • nbf (Not Before): When the token becomes valid
  • iat (Issued At): When the token was issued
  • jti (JWT ID): Unique identifier for the token

Signature

The signature is created by taking the encoded header, the encoded payload, a secret, and signing them with the algorithm specified in the header.

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

JWT Flow in Authentication

sequenceDiagram participant Client participant Server Client->>Server: Login with credentials Server->>Server: Verify credentials Server->>Server: Generate JWT with payload & signature Server->>Client: Return JWT Note over Client,Server: Later requests Client->>Server: Request with JWT in Authorization header Server->>Server: Verify JWT signature Server->>Server: Check token expiration Server->>Client: Return protected resource

HTTP Header Example

JWTs are typically sent in the HTTP Authorization header using the Bearer schema:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Implementing JWTs in Node.js

Let's look at how to implement JWT authentication in a Node.js application using the jsonwebtoken package.

Installation

npm install jsonwebtoken

Creating a JWT Token

const jwt = require('jsonwebtoken');

// Secret key should be stored in environment variables
const SECRET_KEY = 'your-secret-key';

// Function to generate a token
function generateToken(user) {
  const payload = {
    sub: user.id,
    name: user.name,
    email: user.email,
    role: user.role,
    // Standard claims
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hour expiration
  };
  
  return jwt.sign(payload, SECRET_KEY);
}

Verifying a JWT Token

function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    return { valid: true, expired: false, decoded };
  } catch (error) {
    return { 
      valid: false, 
      expired: error.name === 'TokenExpiredError',
      decoded: null
    };
  }
}

Middleware for Express.js

function authMiddleware(req, res, next) {
  // Get token from Authorization header
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'No token provided' });
  }
  
  const token = authHeader.split(' ')[1];
  const { valid, expired, decoded } = verifyToken(token);
  
  if (!valid) {
    return res.status(401).json({
      message: expired ? 'Token has expired' : 'Invalid token'
    });
  }
  
  // Attach user info to request
  req.user = decoded;
  next();
}

Complete Express.js Example

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

app.use(express.json());

const SECRET_KEY = 'your-secret-key';
const PORT = 3000;

// Mock user database
const users = [
  { id: 1, username: 'user1', password: 'password1', role: 'user' },
  { id: 2, username: 'admin', password: 'admin123', role: 'admin' }
];

// Login route
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  // Find user
  const user = users.find(u => u.username === username && u.password === password);
  
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }
  
  // Generate token
  const token = jwt.sign(
    { 
      sub: user.id,
      username: user.username,
      role: user.role,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + (60 * 60)
    },
    SECRET_KEY
  );
  
  res.json({ token });
});

// Auth middleware
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'No token provided' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, SECRET_KEY);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({
      message: error.name === 'TokenExpiredError' 
        ? 'Token has expired' 
        : 'Invalid token'
    });
  }
}

// Protected route
app.get('/protected', authenticate, (req, res) => {
  res.json({ 
    message: 'This is a protected route',
    user: req.user
  });
});

// Role-based access control
app.get('/admin', authenticate, (req, res) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Access denied' });
  }
  
  res.json({ message: 'Admin panel' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

JWT Security Considerations

Best Practices

  • Store the secret key securely: Use environment variables, not hard-coded values
  • Keep tokens secure: Store them in HttpOnly cookies or secure storage
  • Set proper expiration: Balance security and user experience
  • Use HTTPS: Always transmit JWTs over encrypted connections
  • Validate all tokens: Check signature and expiration every time
  • Implement refresh tokens: For longer sessions with better security

Common Vulnerabilities

Important Warning

Never store sensitive information like passwords or credit card details in a JWT payload. The payload is merely encoded (Base64Url), not encrypted, and can be easily decoded.

JWT vs. Session Authentication

Feature JWT Authentication Session Authentication
Storage Client-side Server-side
Scalability Excellent (stateless) Requires session replication
Mobile/API Well-suited Additional work required
Cross-domain Easy Difficult (CORS issues)
Server load Lower (no session lookups) Higher (session verification)
Revocation Difficult (requires blacklist) Easy (delete session)

Advantages & Disadvantages of JWTs

Advantages

Disadvantages

Real-World JWT Applications

Real-World Example: Auth0

Auth0 is a popular authentication service that uses JWTs as part of its authentication flow. It manages token generation, validation, and refresh while providing additional security features like:

  • Token rotation
  • Blacklisting
  • Anomaly detection
  • Centralized token management

Practice Activities

Activity 1: Decode a JWT

Visit jwt.io and paste the following JWT to decode it:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.YDQ70Kbi-OGVhJH3P2dV2cZ6y2ioMYh4iP0kBRHy6YE

Questions:

  1. What algorithm is used for signing?
  2. When does the token expire?
  3. What role does the user have?

Activity 2: Create a Basic JWT Authentication System

Create a simple Express.js application that:

  1. Has a login route that generates JWT tokens
  2. Has a protected route that verifies tokens
  3. Implements role-based access control (admin vs. user)

Use the example code provided above as a starting point.

Activity 3: Implement Refresh Tokens

Extend the basic authentication system to implement a refresh token mechanism:

  1. Create short-lived access tokens (15 minutes)
  2. Create longer-lived refresh tokens (7 days)
  3. Add a route to refresh the access token using the refresh token
  4. Implement a token blacklist for revoked refresh tokens

Additional Resources

Summary

Next, we'll cover practical implementation strategies and token refresh mechanisms.