Express.js + JWT

Automatically extract JWT claims and forward them as headers in Express.js applications.

Basic JWT Middleware

Setup

npm install express jsonwebtoken
const express = require('express')
const jwt = require('jsonwebtoken')
const { Tokenlay } = require('tokenlay')
 
const app = express()
const tokenlay = new Tokenlay({
  apiKey: process.env.TOKENLAY_API_KEY
})
 
app.use(express.json())

JWT Extraction Middleware

// Middleware to extract JWT and set user context
function extractUserContext(req, res, next) {
  const authHeader = req.headers.authorization
  const token = authHeader?.replace('Bearer ', '')
  
  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET)
      
      // Set user context headers
      req.userContext = {
        'User-ID': decoded.sub,
        'User-Email': decoded.email,
        'User-Role': decoded.role || 'user',
        'Organization-ID': decoded.org,
        'Token-Issued': decoded.iat?.toString(),
        'Token-Expires': decoded.exp?.toString()
      }
      
      // Also attach raw claims for additional processing
      req.user = decoded
    } catch (error) {
      console.error('Invalid JWT:', error.message)
      req.userContext = {}
    }
  } else {
    req.userContext = {}
  }
  
  next()
}
 
// Apply to all API routes
app.use('/api', extractUserContext)

API Integration

app.post('/api/process', async (req, res) => {
  try {
    const result = await tokenlay.process(req.body, {
      headers: req.userContext || {}
    })
    
    res.json(result)
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})
 
app.listen(3000, () => {
  console.log('Server running on port 3000')
})

Advanced JWT Handling

Multiple Token Sources

function extractTokenFromRequest(req) {
  // Check Authorization header
  const authHeader = req.headers.authorization
  if (authHeader?.startsWith('Bearer ')) {
    return authHeader.slice(7)
  }
  
  // Check cookies
  if (req.cookies?.access_token) {
    return req.cookies.access_token
  }
  
  // Check query parameter (not recommended for production)
  if (req.query.token) {
    return req.query.token
  }
  
  return null
}
 
function extractUserContext(req, res, next) {
  const token = extractTokenFromRequest(req)
  
  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET)
      
      req.userContext = {
        'User-ID': decoded.sub,
        'User-Email': decoded.email,
        'User-Role': decoded.role || 'user',
        'Organization-ID': decoded.org,
        'Permissions': Array.isArray(decoded.permissions) 
          ? decoded.permissions.join(',') 
          : '',
        'Token-Source': getTokenSource(req)
      }
      
      req.user = decoded
    } catch (error) {
      console.error('JWT verification failed:', error.message)
      req.userContext = {}
    }
  } else {
    req.userContext = {}
  }
  
  next()
}
 
function getTokenSource(req) {
  if (req.headers.authorization) return 'header'
  if (req.cookies?.access_token) return 'cookie'
  if (req.query.token) return 'query'
  return 'none'
}

Token Validation and Refresh

const jwt = require('jsonwebtoken')
const redis = require('redis')
 
const redisClient = redis.createClient()
 
async function validateAndRefreshToken(req, res, next) {
  const token = extractTokenFromRequest(req)
  
  if (!token) {
    req.userContext = {}
    return next()
  }
  
  try {
    // Check if token is blacklisted
    const isBlacklisted = await redisClient.get(`blacklist:${token}`)
    if (isBlacklisted) {
      req.userContext = {}
      return next()
    }
    
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    
    // Check if token expires soon (within 5 minutes)
    const now = Math.floor(Date.now() / 1000)
    const expiresIn = decoded.exp - now
    
    if (expiresIn < 300) { // 5 minutes
      // Add refresh warning header
      res.set('Token-Refresh-Required', 'true')
      res.set('Token-Expires-In', expiresIn.toString())
    }
    
    req.userContext = {
      'User-ID': decoded.sub,
      'User-Email': decoded.email,
      'User-Role': decoded.role,
      'Organization-ID': decoded.org,
      'Token-Expires-In': expiresIn.toString()
    }
    
    req.user = decoded
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      res.set('Token-Expired', 'true')
    }
    
    console.error('Token validation failed:', error.message)
    req.userContext = {}
  }
  
  next()
}

Role-Based Access Control

Permission-Based Headers

function enrichUserContext(req, res, next) {
  if (req.user) {
    const permissions = req.user.permissions || []
    const roles = req.user.roles || [req.user.role]
    
    // Add permission-based headers
    req.userContext = {
      ...req.userContext,
      'User-Permissions': permissions.join(','),
      'User-Roles': roles.join(','),
      'Is-Admin': roles.includes('admin') ? 'true' : 'false',
      'Can-Write': permissions.includes('write') ? 'true' : 'false',
      'Access-Level': calculateAccessLevel(roles, permissions)
    }
  }
  
  next()
}
 
function calculateAccessLevel(roles, permissions) {
  if (roles.includes('admin')) return 'full'
  if (permissions.includes('write')) return 'write'
  if (permissions.includes('read')) return 'read'
  return 'none'
}
 
// Apply after user context extraction
app.use('/api', extractUserContext, enrichUserContext)

Route-Specific Context

function createRouteSpecificContext(requiredRole = null) {
  return (req, res, next) => {
    if (requiredRole && req.user?.role !== requiredRole) {
      return res.status(403).json({ error: 'Insufficient permissions' })
    }
    
    // Add route-specific context
    req.userContext = {
      ...req.userContext,
      'Route-Required-Role': requiredRole || 'none',
      'Route-Access-Granted': 'true',
      'Request-Path': req.path,
      'Request-Method': req.method
    }
    
    next()
  }
}
 
// Usage
app.post('/api/admin/process', 
  createRouteSpecificContext('admin'),
  async (req, res) => {
    const result = await tokenlay.process(req.body, {
      headers: req.userContext
    })
    res.json(result)
  }
)

Error Handling and Logging

Comprehensive Error Handling

function handleAuthErrors(req, res, next) {
  const authHeader = req.headers.authorization
  const token = authHeader?.replace('Bearer ', '')
  
  if (!token) {
    req.userContext = {
      'Auth-Status': 'no-token',
      'User-ID': 'anonymous'
    }
    return next()
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    
    req.userContext = {
      'Auth-Status': 'authenticated',
      'User-ID': decoded.sub,
      'User-Email': decoded.email,
      'User-Role': decoded.role,
      'Organization-ID': decoded.org
    }
    
    req.user = decoded
  } catch (error) {
    let authStatus = 'invalid-token'
    
    switch (error.name) {
      case 'TokenExpiredError':
        authStatus = 'token-expired'
        break
      case 'JsonWebTokenError':
        authStatus = 'malformed-token'
        break
      case 'NotBeforeError':
        authStatus = 'token-not-active'
        break
    }
    
    req.userContext = {
      'Auth-Status': authStatus,
      'Auth-Error': error.message,
      'User-ID': 'anonymous'
    }
    
    // Log authentication failures
    console.warn('Authentication failed:', {
      error: error.message,
      ip: req.ip,
      userAgent: req.get('User-Agent'),
      path: req.path
    })
  }
  
  next()
}

Request Logging

function logRequestContext(req, res, next) {
  const startTime = Date.now()
  
  // Add request tracking
  const requestId = generateRequestId()
  req.userContext = {
    ...req.userContext,
    'Request-ID': requestId,
    'Request-Timestamp': new Date().toISOString(),
    'Client-IP': req.ip,
    'User-Agent': req.get('User-Agent')?.substring(0, 100) // Truncate for header size
  }
  
  // Log request
  console.log('Request started:', {
    requestId,
    method: req.method,
    path: req.path,
    userId: req.userContext['User-ID'],
    ip: req.ip
  })
  
  // Log response when finished
  res.on('finish', () => {
    const duration = Date.now() - startTime
    console.log('Request completed:', {
      requestId,
      statusCode: res.statusCode,
      duration,
      userId: req.userContext['User-ID']
    })
  })
  
  next()
}
 
function generateRequestId() {
  return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}

Performance Optimization

Token Caching

const NodeCache = require('node-cache')
 
// Cache decoded tokens for 5 minutes
const tokenCache = new NodeCache({ stdTTL: 300 })
 
function cacheEnabledUserContext(req, res, next) {
  const token = extractTokenFromRequest(req)
  
  if (!token) {
    req.userContext = {}
    return next()
  }
  
  // Check cache first
  const cachedContext = tokenCache.get(token)
  if (cachedContext) {
    req.userContext = cachedContext
    req.user = cachedContext._user // Store user separately
    return next()
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    
    const userContext = {
      'User-ID': decoded.sub,
      'User-Email': decoded.email,
      'User-Role': decoded.role,
      'Organization-ID': decoded.org,
      '_user': decoded // Internal use only
    }
    
    // Cache for future requests
    tokenCache.set(token, userContext)
    
    req.userContext = userContext
    req.user = decoded
  } catch (error) {
    console.error('JWT verification failed:', error.message)
    req.userContext = {}
  }
  
  next()
}

Async Context Processing

async function asyncUserContext(req, res, next) {
  const token = extractTokenFromRequest(req)
  
  if (!token) {
    req.userContext = {}
    return next()
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    
    // Fetch additional user data asynchronously
    const [userProfile, permissions] = await Promise.all([
      fetchUserProfile(decoded.sub),
      fetchUserPermissions(decoded.sub)
    ])
    
    req.userContext = {
      'User-ID': decoded.sub,
      'User-Email': decoded.email,
      'User-Role': decoded.role,
      'Organization-ID': decoded.org,
      'User-Department': userProfile?.department,
      'User-Permissions': permissions?.join(',') || '',
      'Profile-Complete': userProfile ? 'true' : 'false'
    }
    
    req.user = decoded
  } catch (error) {
    console.error('User context processing failed:', error.message)
    req.userContext = {}
  }
  
  next()
}
 
async function fetchUserProfile(userId) {
  // Implementation depends on your user store
  // Could be database, external API, etc.
  try {
    const response = await fetch(`${process.env.USER_API_URL}/users/${userId}`)
    return response.ok ? await response.json() : null
  } catch {
    return null
  }
}
 
async function fetchUserPermissions(userId) {
  try {
    const response = await fetch(`${process.env.AUTH_API_URL}/permissions/${userId}`)
    return response.ok ? await response.json() : []
  } catch {
    return []
  }
}

Testing

Mock Authentication for Tests

// test/middleware.test.js
const request = require('supertest')
const jwt = require('jsonwebtoken')
const app = require('../app')
 
describe('User Context Middleware', () => {
  const mockUser = {
    sub: 'user-123',
    email: 'test@example.com',
    role: 'admin',
    org: 'org-456'
  }
  
  const mockToken = jwt.sign(mockUser, process.env.JWT_SECRET || 'test-secret')
  
  it('should extract user context from valid JWT', async () => {
    const response = await request(app)
      .post('/api/process')
      .set('Authorization', `Bearer ${mockToken}`)
      .send({ data: 'test' })
      .expect(200)
    
    // Verify that user context was properly extracted
    expect(response.body).toBeDefined()
  })
  
  it('should handle missing token gracefully', async () => {
    const response = await request(app)
      .post('/api/process')
      .send({ data: 'test' })
      .expect(200)
    
    // Should still process but with anonymous context
    expect(response.body).toBeDefined()
  })
  
  it('should reject expired tokens', async () => {
    const expiredToken = jwt.sign(
      { ...mockUser, exp: Math.floor(Date.now() / 1000) - 3600 }, // 1 hour ago
      process.env.JWT_SECRET || 'test-secret'
    )
    
    const response = await request(app)
      .post('/api/process')
      .set('Authorization', `Bearer ${expiredToken}`)
      .send({ data: 'test' })
      .expect(200)
    
    // Should process with anonymous context due to expired token
    expect(response.body).toBeDefined()
  })
})

Best Practices

Security

  • Always use HTTPS in production
  • Validate all JWT claims before using them
  • Implement proper token blacklisting for logout
  • Use short-lived tokens with refresh mechanisms
  • Never log full tokens or sensitive claims

Performance

  • Cache decoded tokens when appropriate
  • Use async/await for non-blocking operations
  • Consider token validation performance impact
  • Implement efficient user data fetching

Monitoring

  • Log authentication events for security analysis
  • Monitor token expiration patterns
  • Track failed authentication attempts
  • Alert on unusual authentication patterns