Home / Notebooks / Security
Security
intermediate

JWT Essentials

Essential JSON Web Token concepts for secure authentication and authorization

March 10, 2024
Updated regularly

JWT Essentials

Quick reference guide for JSON Web Tokens (JWT) - secure token-based authentication.

What is JWT?

JWT (JSON Web Token) is an open standard (RFC 7519) for securely transmitting information between parties as a JSON object:

  • Self-contained - Contains all necessary information
  • Digitally signed - Can verify authenticity and integrity
  • Compact - Easy to transmit via URL, POST, or HTTP header
  • Stateless - Server doesn't need to store session data
  • Platform-agnostic - Works across different technologies
  • Scalable - No server-side session storage needed
  • JWT Structure

    Three Parts

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

    header.payload.signature
    
    Example:
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    1. Header

    Contains metadata about the token:

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

    Common algorithms:

  • HS256 - HMAC with SHA-256 (symmetric)
  • RS256 - RSA with SHA-256 (asymmetric)
  • ES256 - ECDSA with SHA-256 (asymmetric)
  • 2. Payload

    Contains claims (statements about the user):

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

    Registered Claims:

  • iss - Issuer
  • sub - Subject (user ID)
  • aud - Audience
  • exp - Expiration time
  • nbf - Not before
  • iat - Issued at
  • jti - JWT ID
  • Public Claims:

  • Custom claims (e.g., name, email, role)
  • Private Claims:

  • Custom claims for specific use between parties
  • 3. Signature

    Verifies the token hasn't been tampered with:

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

    How JWT Works

    Authentication Flow

    ┌──────────┐                          ┌──────────┐
    │  Client  │                          │  Server  │
    └────┬─────┘                          └────┬─────┘
         │                                     │
         │  1. Login (credentials)             │
         │────────────────────────────────────>│
         │                                     │
         │                    2. Verify & Generate JWT
         │                                     │
         │  3. Return JWT                      │
         │<────────────────────────────────────│
         │                                     │
         │  4. Request with JWT in header      │
         │────────────────────────────────────>│
         │                                     │
         │                    5. Verify JWT & Process
         │                                     │
         │  6. Return protected resource       │
         │<────────────────────────────────────│
    

    Creating JWTs

    Node.js (jsonwebtoken)

    const jwt = require('jsonwebtoken');
    
    // ========== Create Token ==========
    const payload = {
      sub: '1234567890',
      name: 'John Doe',
      email: 'john@example.com',
      role: 'admin'
    };
    
    const secret = 'your-secret-key';
    const options = {
      expiresIn: '1h',
      issuer: 'your-app',
      audience: 'your-api'
    };
    
    const token = jwt.sign(payload, secret, options);
    console.log(token);
    
    // ========== With RSA (Asymmetric) ==========
    const privateKey = fs.readFileSync('private.key');
    const token = jwt.sign(payload, privateKey, {
      algorithm: 'RS256',
      expiresIn: '1h'
    });
    

    Python (PyJWT)

    import jwt
    from datetime import datetime, timedelta
    
    # ========== Create Token ==========
    payload = {
        'sub': '1234567890',
        'name': 'John Doe',
        'email': 'john@example.com',
        'exp': datetime.utcnow() + timedelta(hours=1),
        'iat': datetime.utcnow()
    }
    
    secret = 'your-secret-key'
    token = jwt.encode(payload, secret, algorithm='HS256')
    print(token)
    
    # ========== With RSA ==========
    with open('private.key', 'rb') as f:
        private_key = f.read()
    
    token = jwt.encode(payload, private_key, algorithm='RS256')
    

    Java (jjwt)

    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import java.util.Date;
    
    // ========== Create Token ==========
    String secret = "your-secret-key";
    
    String token = Jwts.builder()
        .setSubject("1234567890")
        .claim("name", "John Doe")
        .claim("email", "john@example.com")
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + 3600000))
        .signWith(SignatureAlgorithm.HS256, secret)
        .compact();
    
    System.out.println(token);
    

    Go (jwt-go)

    package main
    
    import (
        "fmt"
        "time"
        "github.com/golang-jwt/jwt/v5"
    )
    
    // ========== Create Token ==========
    func createToken() string {
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
            "sub":   "1234567890",
            "name":  "John Doe",
            "email": "john@example.com",
            "exp":   time.Now().Add(time.Hour * 1).Unix(),
            "iat":   time.Now().Unix(),
        })
    
        secret := []byte("your-secret-key")
        tokenString, err := token.SignedString(secret)
        if err != nil {
            panic(err)
        }
    
        return tokenString
    }
    

    Verifying JWTs

    Node.js

    const jwt = require('jsonwebtoken');
    
    // ========== Verify Token ==========
    try {
      const decoded = jwt.verify(token, 'your-secret-key');
      console.log('Decoded:', decoded);
      // Access claims
      console.log('User ID:', decoded.sub);
      console.log('Name:', decoded.name);
    } catch (err) {
      if (err.name === 'TokenExpiredError') {
        console.error('Token expired');
      } else if (err.name === 'JsonWebTokenError') {
        console.error('Invalid token');
      } else {
        console.error('Verification failed:', err);
      }
    }
    
    // ========== With Options ==========
    const options = {
      issuer: 'your-app',
      audience: 'your-api',
      algorithms: ['HS256']
    };
    
    const decoded = jwt.verify(token, secret, options);
    
    // ========== Verify RSA ==========
    const publicKey = fs.readFileSync('public.key');
    const decoded = jwt.verify(token, publicKey, {
      algorithms: ['RS256']
    });
    

    Python

    import jwt
    from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
    
    # ========== Verify Token ==========
    try:
        decoded = jwt.decode(
            token,
            'your-secret-key',
            algorithms=['HS256'],
            issuer='your-app',
            audience='your-api'
        )
        print('Decoded:', decoded)
    except ExpiredSignatureError:
        print('Token expired')
    except InvalidTokenError:
        print('Invalid token')
    
    # ========== Verify RSA ==========
    with open('public.key', 'rb') as f:
        public_key = f.read()
    
    decoded = jwt.decode(token, public_key, algorithms=['RS256'])
    

    Java

    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    
    // ========== Verify Token ==========
    try {
        Claims claims = Jwts.parser()
            .setSigningKey("your-secret-key")
            .parseClaimsJws(token)
            .getBody();
        
        System.out.println("User ID: " + claims.getSubject());
        System.out.println("Name: " + claims.get("name"));
    } catch (ExpiredJwtException e) {
        System.err.println("Token expired");
    } catch (JwtException e) {
        System.err.println("Invalid token");
    }
    

    Express.js Middleware

    Authentication Middleware

    const jwt = require('jsonwebtoken');
    
    // ========== Auth Middleware ==========
    function authenticateToken(req, res, next) {
      // Get token from header
      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, process.env.JWT_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/protected', authenticateToken, (req, res) => {
      res.json({
        message: 'Protected data',
        user: req.user
      });
    });
    
    // ========== Login Route ==========
    app.post('/api/login', async (req, res) => {
      const { email, password } = req.body;
    
      // Verify credentials (simplified)
      const user = await User.findOne({ email });
      if (!user || !await user.comparePassword(password)) {
        return res.status(401).json({ error: 'Invalid credentials' });
      }
    
      // Generate token
      const token = jwt.sign(
        { 
          sub: user.id,
          email: user.email,
          role: user.role
        },
        process.env.JWT_SECRET,
        { expiresIn: '1h' }
      );
    
      res.json({ token });
    });
    

    Role-Based Authorization

    // ========== Role Middleware ==========
    function requireRole(...roles) {
      return (req, res, next) => {
        if (!req.user) {
          return res.status(401).json({ error: 'Not authenticated' });
        }
    
        if (!roles.includes(req.user.role)) {
          return res.status(403).json({ error: 'Insufficient permissions' });
        }
    
        next();
      };
    }
    
    // ========== Protected Admin Route ==========
    app.get('/api/admin', 
      authenticateToken, 
      requireRole('admin'),
      (req, res) => {
        res.json({ message: 'Admin data' });
      }
    );
    
    // ========== Protected Multi-Role Route ==========
    app.get('/api/dashboard', 
      authenticateToken, 
      requireRole('admin', 'manager'),
      (req, res) => {
        res.json({ message: 'Dashboard data' });
      }
    );
    

    Refresh Tokens

    Implementation

    const jwt = require('jsonwebtoken');
    const crypto = require('crypto');
    
    // ========== Generate Token Pair ==========
    function generateTokens(user) {
      // Access token (short-lived)
      const accessToken = jwt.sign(
        { 
          sub: user.id,
          email: user.email,
          role: user.role
        },
        process.env.JWT_SECRET,
        { expiresIn: '15m' }
      );
    
      // Refresh token (long-lived)
      const refreshToken = crypto.randomBytes(40).toString('hex');
    
      // Store refresh token in database with expiry
      // (simplified - use proper database)
      refreshTokens.set(refreshToken, {
        userId: user.id,
        expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days
      });
    
      return { accessToken, refreshToken };
    }
    
    // ========== Login Endpoint ==========
    app.post('/api/login', async (req, res) => {
      const { email, password } = req.body;
    
      const user = await User.findOne({ email });
      if (!user || !await user.comparePassword(password)) {
        return res.status(401).json({ error: 'Invalid credentials' });
      }
    
      const tokens = generateTokens(user);
      
      res.json({
        accessToken: tokens.accessToken,
        refreshToken: tokens.refreshToken
      });
    });
    
    // ========== Refresh Endpoint ==========
    app.post('/api/refresh', async (req, res) => {
      const { refreshToken } = req.body;
    
      if (!refreshToken) {
        return res.status(401).json({ error: 'Refresh token required' });
      }
    
      // Check if refresh token exists and is valid
      const storedToken = refreshTokens.get(refreshToken);
      if (!storedToken || storedToken.expiresAt < Date.now()) {
        return res.status(403).json({ error: 'Invalid refresh token' });
      }
    
      // Get user
      const user = await User.findById(storedToken.userId);
      if (!user) {
        return res.status(403).json({ error: 'User not found' });
      }
    
      // Generate new token pair
      const tokens = generateTokens(user);
    
      // Invalidate old refresh token
      refreshTokens.delete(refreshToken);
    
      res.json({
        accessToken: tokens.accessToken,
        refreshToken: tokens.refreshToken
      });
    });
    
    // ========== Logout Endpoint ==========
    app.post('/api/logout', (req, res) => {
      const { refreshToken } = req.body;
      
      if (refreshToken) {
        refreshTokens.delete(refreshToken);
      }
    
      res.json({ message: 'Logged out successfully' });
    });
    

    Client-Side Token Management

    // ========== Token Manager ==========
    class TokenManager {
      constructor() {
        this.accessToken = localStorage.getItem('accessToken');
        this.refreshToken = localStorage.getItem('refreshToken');
      }
    
      // Save tokens
      setTokens(accessToken, refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        localStorage.setItem('accessToken', accessToken);
        localStorage.setItem('refreshToken', refreshToken);
      }
    
      // Clear tokens
      clearTokens() {
        this.accessToken = null;
        this.refreshToken = null;
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
      }
    
      // Get access token
      getAccessToken() {
        return this.accessToken;
      }
    
      // Refresh access token
      async refreshAccessToken() {
        try {
          const response = await fetch('/api/refresh', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ refreshToken: this.refreshToken })
          });
    
          if (!response.ok) {
            throw new Error('Failed to refresh token');
          }
    
          const data = await response.json();
          this.setTokens(data.accessToken, data.refreshToken);
          return data.accessToken;
        } catch (error) {
          this.clearTokens();
          throw error;
        }
      }
    }
    
    // ========== API Client with Auto-Refresh ==========
    const tokenManager = new TokenManager();
    
    async function apiCall(url, options = {}) {
      // Add access token to request
      const headers = {
        ...options.headers,
        'Authorization': `Bearer ${tokenManager.getAccessToken()}`
      };
    
      let response = await fetch(url, { ...options, headers });
    
      // If token expired, refresh and retry
      if (response.status === 401) {
        try {
          const newToken = await tokenManager.refreshAccessToken();
          headers['Authorization'] = `Bearer ${newToken}`;
          response = await fetch(url, { ...options, headers });
        } catch (error) {
          // Redirect to login
          window.location.href = '/login';
          throw error;
        }
      }
    
      return response;
    }
    
    // Usage
    apiCall('/api/protected')
      .then(res => res.json())
      .then(data => console.log(data))
      .catch(err => console.error(err));
    

    Security Best Practices

    Token Storage

    // ========== ✅ GOOD: Secure HTTP-only Cookie ==========
    app.post('/api/login', async (req, res) => {
      const tokens = generateTokens(user);
    
      // Store refresh token in HTTP-only cookie
      res.cookie('refreshToken', tokens.refreshToken, {
        httpOnly: true,
        secure: true, // HTTPS only
        sameSite: 'strict',
        maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
      });
    
      res.json({ accessToken: tokens.accessToken });
    });
    
    // ========== ❌ BAD: localStorage for sensitive tokens ==========
    // localStorage is vulnerable to XSS attacks
    localStorage.setItem('refreshToken', refreshToken); // Don't do this!
    

    Secret Key Management

    // ========== ✅ GOOD: Environment Variables ==========
    const secret = process.env.JWT_SECRET;
    
    // ========== ✅ GOOD: Strong Secret ==========
    // Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
    // Example: 4f8a3c2e1d9b7a6f5e4d3c2b1a9e8d7c6b5a4f3e2d1c9b8a7f6e5d4c3b2a1f0
    
    // ========== ❌ BAD: Hardcoded Secret ==========
    const secret = 'mysecret'; // Too simple!
    const secret = 'your-secret-key'; // Never commit to git!
    

    Token Validation

    // ========== Complete Validation ==========
    function validateToken(token) {
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET, {
          algorithms: ['HS256'], // Specify allowed algorithms
          issuer: 'your-app',
          audience: 'your-api'
        });
    
        // Check expiration
        if (decoded.exp < Date.now() / 1000) {
          throw new Error('Token expired');
        }
    
        // Check not before
        if (decoded.nbf && decoded.nbf > Date.now() / 1000) {
          throw new Error('Token not yet valid');
        }
    
        // Additional checks
        if (!decoded.sub) {
          throw new Error('Missing subject');
        }
    
        return decoded;
      } catch (error) {
        throw new Error(`Token validation failed: ${error.message}`);
      }
    }
    

    Algorithm Confusion Attack

    // ========== ✅ GOOD: Specify Algorithm ==========
    jwt.verify(token, secret, { algorithms: ['HS256'] });
    
    // ========== ❌ BAD: No Algorithm Specified ==========
    // Attacker can change algorithm in header to "none"
    jwt.verify(token, secret);
    

    Payload Security

    // ========== ✅ GOOD: Minimal Payload ==========
    const payload = {
      sub: user.id,
      role: user.role,
      exp: Math.floor(Date.now() / 1000) + 3600
    };
    
    // ========== ❌ BAD: Sensitive Data ==========
    const payload = {
      sub: user.id,
      password: user.password, // Never include passwords!
      ssn: user.ssn, // Never include sensitive PII!
      creditCard: user.creditCard // Never include payment info!
    };
    

    Common Use Cases

    API Authentication

    // ========== Header Format ==========
    // Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    
    // ========== Client Request ==========
    fetch('/api/users', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    })
    .then(res => res.json())
    .then(data => console.log(data));
    

    Single Sign-On (SSO)

    // ========== Identity Provider ==========
    app.post('/sso/login', async (req, res) => {
      const user = await authenticateUser(req.body);
    
      const token = jwt.sign(
        { 
          sub: user.id,
          email: user.email,
          name: user.name
        },
        process.env.JWT_SECRET,
        { 
          expiresIn: '1h',
          issuer: 'identity-provider',
          audience: ['app1', 'app2', 'app3']
        }
      );
    
      res.redirect(`https://app1.example.com/callback?token=${token}`);
    });
    
    // ========== Service Provider ==========
    app.get('/callback', (req, res) => {
      const token = req.query.token;
    
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET, {
          issuer: 'identity-provider',
          audience: 'app1'
        });
    
        // Create local session
        req.session.user = decoded;
        res.redirect('/dashboard');
      } catch (error) {
        res.redirect('/login?error=invalid_token');
      }
    });
    

    Stateless Sessions

    // ========== Create Session ==========
    app.post('/api/login', async (req, res) => {
      const user = await authenticateUser(req.body);
    
      const sessionToken = jwt.sign(
        {
          sub: user.id,
          email: user.email,
          role: user.role,
          sessionId: crypto.randomUUID()
        },
        process.env.JWT_SECRET,
        { expiresIn: '24h' }
      );
    
      res.cookie('session', sessionToken, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict'
      });
    
      res.json({ message: 'Logged in' });
    });
    
    // ========== Session Middleware ==========
    function sessionMiddleware(req, res, next) {
      const token = req.cookies.session;
    
      if (!token) {
        return res.status(401).json({ error: 'Not authenticated' });
      }
    
      try {
        req.session = jwt.verify(token, process.env.JWT_SECRET);
        next();
      } catch (error) {
        res.clearCookie('session');
        return res.status(401).json({ error: 'Invalid session' });
      }
    }
    

    JWT vs Sessions

    JWT Advantages

  • Stateless - No server-side storage
  • Scalable - Works across multiple servers
  • Mobile-friendly - Easy to use in mobile apps
  • Cross-domain - Can be used across different domains
  • Self-contained - Contains all user information
  • Session Advantages

  • Revocable - Can invalidate immediately
  • Smaller overhead - Just session ID
  • More secure - Can't be decoded by client
  • Server control - Full control over session data
  • When to Use JWT

  • Microservices architecture
  • Mobile applications
  • Third-party API access
  • Cross-domain authentication
  • Stateless REST APIs
  • When to Use Sessions

  • Traditional web applications
  • Need immediate revocation
  • Sensitive data
  • Simple authentication needs
  • Common Vulnerabilities

    1. Weak Secret Key

    // ❌ Vulnerable
    const secret = 'secret';
    
    // ✅ Secure
    const secret = process.env.JWT_SECRET; // 32+ character random string
    

    2. No Expiration

    // ❌ Vulnerable
    const token = jwt.sign(payload, secret); // No expiration!
    
    // ✅ Secure
    const token = jwt.sign(payload, secret, { expiresIn: '1h' });
    

    3. Sensitive Data in Payload

    // ❌ Vulnerable
    const payload = { userId: 1, password: 'secret123' };
    
    // ✅ Secure
    const payload = { sub: 1, role: 'user' };
    

    4. Not Validating Algorithms

    // ❌ Vulnerable
    jwt.verify(token, secret);
    
    // ✅ Secure
    jwt.verify(token, secret, { algorithms: ['HS256'] });
    

    5. Storing in localStorage

    // ❌ Vulnerable to XSS
    localStorage.setItem('token', token);
    
    // ✅ More secure
    // Use HTTP-only cookies or memory storage
    

    Debugging JWTs

    Decode Without Verification

    // ========== Node.js ==========
    const jwt = require('jsonwebtoken');
    const decoded = jwt.decode(token, { complete: true });
    
    console.log('Header:', decoded.header);
    console.log('Payload:', decoded.payload);
    console.log('Signature:', decoded.signature);
    
    // ========== Manual Decode ==========
    const [header, payload, signature] = token.split('.');
    
    const decodedHeader = JSON.parse(
      Buffer.from(header, 'base64url').toString()
    );
    const decodedPayload = JSON.parse(
      Buffer.from(payload, 'base64url').toString()
    );
    
    console.log('Header:', decodedHeader);
    console.log('Payload:', decodedPayload);
    

    Online Tools

  • jwt.io - Decode, verify, and generate JWTs
  • jwt.ms - Microsoft JWT decoder
  • Logging

    // ========== Debug Middleware ==========
    function logJWT(req, res, next) {
      const token = req.headers.authorization?.split(' ')[1];
      
      if (token) {
        const decoded = jwt.decode(token, { complete: true });
        console.log('JWT Debug:', {
          header: decoded.header,
          payload: decoded.payload,
          issuedAt: new Date(decoded.payload.iat * 1000),
          expiresAt: new Date(decoded.payload.exp * 1000),
          isExpired: decoded.payload.exp < Date.now() / 1000
        });
      }
      
      next();
    }
    
    app.use(logJWT);
    

    Best Practices Summary

  • Use strong, random secrets (32+ characters)
  • Always set expiration (short-lived tokens)
  • Use refresh tokens for long-lived sessions
  • Validate algorithms explicitly
  • Use HTTPS in production
  • Don't store sensitive data in payload
  • Store tokens securely (HTTP-only cookies for web)
  • Implement token revocation for critical applications
  • Use standard claims (iss, sub, aud, exp)
  • Monitor and log token usage
  • Resources

  • JWT.io - Official JWT website
  • RFC 7519 - JWT specification
  • OWASP JWT Cheat Sheet
  • jwt.io Libraries - JWT libraries for all languages
  • Topics

    JWTAuthenticationSecurityAPIAuthorization

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.