Skip to content

JWT Role Based Middleware Examples

Doug Fennell edited this page Oct 14, 2025 · 1 revision

JWT Role-Based Middleware Examples

This page provides complete implementations of JWT-based middleware that enforces tiered authorization policies based on JWT claims and HTTP methods. These examples are perfect for implementing role-based access control (RBAC) for RDCP endpoints.

Basic JWT Role-Based Middleware

Express.js Tiered Authorization Middleware

This middleware enforces that GET requests to control endpoints are allowed for users with role: 'viewer', while PUT or POST requests require role: 'admin':

// File: middleware/jwt-rbac-middleware.js
import jwt from 'jsonwebtoken'

/**
 * Creates JWT-based RBAC middleware for RDCP endpoints
 * @param {Object} options Configuration options
 * @param {string} options.secret JWT secret key
 * @param {Object} options.roles Role configuration
 * @param {string[]} options.roles.viewer Methods allowed for viewer role
 * @param {string[]} options.roles.admin Methods allowed for admin role
 */
export function createRDCPJWTMiddleware(options = {}) {
  const {
    secret = process.env.JWT_SECRET,
    roles = {
      viewer: ['GET'],
      admin: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
    }
  } = options

  if (!secret) {
    throw new Error('JWT_SECRET is required for RDCP JWT middleware')
  }

  return async (req, res, next) => {
    try {
      // Extract JWT token from Authorization header
      const authHeader = req.headers.authorization
      if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({
          error: {
            code: 'RDCP_AUTH_REQUIRED',
            message: 'Bearer token required',
            protocol: 'rdcp/1.0'
          }
        })
      }

      const token = authHeader.substring(7)
      
      // Verify and decode JWT
      let decoded
      try {
        decoded = jwt.verify(token, secret)
      } catch (jwtError) {
        return res.status(401).json({
          error: {
            code: 'RDCP_AUTH_FAILED',
            message: 'Invalid or expired token',
            details: { reason: jwtError.message },
            protocol: 'rdcp/1.0'
          }
        })
      }

      // Extract user role from JWT claims
      const userRole = decoded.role
      if (!userRole) {
        return res.status(403).json({
          error: {
            code: 'RDCP_FORBIDDEN',
            message: 'No role claim found in token',
            protocol: 'rdcp/1.0'
          }
        })
      }

      // Check if user role exists in our configuration
      const allowedMethods = roles[userRole]
      if (!allowedMethods) {
        return res.status(403).json({
          error: {
            code: 'RDCP_FORBIDDEN',
            message: `Unknown role: ${userRole}`,
            details: { 
              providedRole: userRole,
              supportedRoles: Object.keys(roles)
            },
            protocol: 'rdcp/1.0'
          }
        })
      }

      // Check if current HTTP method is allowed for user's role
      const requestMethod = req.method
      if (!allowedMethods.includes(requestMethod)) {
        return res.status(403).json({
          error: {
            code: 'RDCP_FORBIDDEN',
            message: `Method ${requestMethod} not allowed for role ${userRole}`,
            details: {
              requestedMethod: requestMethod,
              userRole: userRole,
              allowedMethods: allowedMethods,
              requiredRole: getRequiredRoleForMethod(requestMethod, roles)
            },
            protocol: 'rdcp/1.0'
          }
        })
      }

      // Attach auth context to request for downstream usage
      req.rdcpAuth = {
        valid: true,
        method: 'bearer',
        userId: decoded.sub || decoded.userId,
        userRole: userRole,
        scopes: decoded.scopes || [],
        sessionId: decoded.jti,
        expiresAt: new Date(decoded.exp * 1000).toISOString()
      }

      next()
    } catch (error) {
      console.error('JWT RBAC middleware error:', error)
      res.status(500).json({
        error: {
          code: 'RDCP_INTERNAL_ERROR',
          message: 'Authentication system error',
          protocol: 'rdcp/1.0'
        }
      })
    }
  }
}

// Helper function to determine required role for a method
function getRequiredRoleForMethod(method, roles) {
  for (const [role, methods] of Object.entries(roles)) {
    if (methods.includes(method)) {
      return role
    }
  }
  return null
}

Complete Express Server Integration

// File: server.js
import express from 'express'
import { adapters, auth } from '@rdcp.dev/server'
import { createRDCPJWTMiddleware } from './middleware/jwt-rbac-middleware.js'

const app = express()
app.use(express.json())

// Create JWT RBAC middleware
const jwtRbacMiddleware = createRDCPJWTMiddleware({
  secret: process.env.JWT_SECRET,
  roles: {
    viewer: ['GET'],           // Viewers can only read
    admin: ['GET', 'POST', 'PUT', 'DELETE']  // Admins can do everything
  }
})

// Apply JWT middleware only to RDCP control endpoints
app.use('/rdcp/v1/control', jwtRbacMiddleware)
app.use('/rdcp/v1/controls', jwtRbacMiddleware) // Custom control endpoints

// Add RDCP middleware (discovery, status, health don't need special auth)
const rdcpMiddleware = adapters.express.createRDCPMiddleware({
  authenticator: (req) => {
    // Use the auth context set by JWT middleware if available
    if (req.rdcpAuth) {
      return req.rdcpAuth
    }
    // Fallback to basic API key auth for non-control endpoints
    return auth.validateRDCPAuth(req)
  },
  capabilities: {
    audit: {
      enabled: true,
      sink: 'console'
    }
  }
})

app.use(rdcpMiddleware)

// Test routes to demonstrate the authorization
app.get('/test/viewer', jwtRbacMiddleware, (req, res) => {
  res.json({
    message: 'Viewer access granted',
    auth: req.rdcpAuth
  })
})

app.post('/test/admin', jwtRbacMiddleware, (req, res) => {
  res.json({
    message: 'Admin access granted',
    auth: req.rdcpAuth
  })
})

app.listen(3000, () => {
  console.log('RDCP server with JWT RBAC running on port 3000')
  console.log('Roles:')
  console.log('  viewer: GET access only')
  console.log('  admin:  Full access (GET, POST, PUT, DELETE)')
})

Advanced Role-Based Authorization Patterns

Granular Endpoint-Specific Authorization

// File: middleware/granular-auth-middleware.js
import jwt from 'jsonwebtoken'

/**
 * Advanced RDCP authorization with granular endpoint and method control
 */
export function createGranularRDCPAuth(config) {
  const {
    secret = process.env.JWT_SECRET,
    endpoints = {
      '/rdcp/v1/discovery': { viewer: ['GET'], admin: ['GET'] },
      '/rdcp/v1/status': { viewer: ['GET'], admin: ['GET'] },
      '/rdcp/v1/control': { admin: ['POST', 'PUT'] },  // Only admins can control
      '/rdcp/v1/health': { viewer: ['GET'], admin: ['GET'] },
      '/rdcp/v1/controls/*': { admin: ['GET', 'POST', 'PUT', 'DELETE'] }
    }
  } = config

  return async (req, res, next) => {
    const token = req.headers.authorization?.replace('Bearer ', '')
    
    if (!token) {
      return res.status(401).json({
        error: {
          code: 'RDCP_AUTH_REQUIRED',
          message: 'JWT token required',
          protocol: 'rdcp/1.0'
        }
      })
    }

    try {
      const decoded = jwt.verify(token, secret)
      const userRole = decoded.role
      const requestPath = req.path
      const requestMethod = req.method

      // Find matching endpoint configuration
      const endpointConfig = findEndpointConfig(requestPath, endpoints)
      if (!endpointConfig) {
        return res.status(404).json({
          error: {
            code: 'RDCP_NOT_FOUND',
            message: 'Endpoint not found',
            protocol: 'rdcp/1.0'
          }
        })
      }

      // Check if user's role is allowed for this endpoint and method
      const allowedMethods = endpointConfig[userRole]
      if (!allowedMethods || !allowedMethods.includes(requestMethod)) {
        return res.status(403).json({
          error: {
            code: 'RDCP_FORBIDDEN',
            message: `${requestMethod} ${requestPath} requires higher privileges`,
            details: {
              userRole,
              requestMethod,
              requestPath,
              availableRoles: Object.keys(endpointConfig)
            },
            protocol: 'rdcp/1.0'
          }
        })
      }

      // Set auth context
      req.rdcpAuth = {
        valid: true,
        method: 'bearer',
        userId: decoded.sub,
        userRole,
        endpoint: requestPath,
        allowedMethods: allowedMethods
      }

      next()
    } catch (error) {
      res.status(401).json({
        error: {
          code: 'RDCP_AUTH_FAILED',
          message: 'Token validation failed',
          details: { reason: error.message },
          protocol: 'rdcp/1.0'
        }
      })
    }
  }
}

function findEndpointConfig(path, endpoints) {
  // Exact match first
  if (endpoints[path]) {
    return endpoints[path]
  }
  
  // Wildcard match
  for (const [pattern, config] of Object.entries(endpoints)) {
    if (pattern.endsWith('*')) {
      const prefix = pattern.slice(0, -1)
      if (path.startsWith(prefix)) {
        return config
      }
    }
  }
  
  return null
}

Scope-Based Authorization

// File: middleware/scope-based-auth.js
import jwt from 'jsonwebtoken'

/**
 * JWT middleware with scope-based authorization for RDCP
 * Supports both role and scope-based access control
 */
export function createScopeBasedAuth(options = {}) {
  const {
    secret = process.env.JWT_SECRET,
    scopeMap = {
      'rdcp:read': ['GET'],
      'rdcp:control': ['POST', 'PUT'],
      'rdcp:admin': ['GET', 'POST', 'PUT', 'DELETE']
    },
    roleToScopes = {
      viewer: ['rdcp:read'],
      admin: ['rdcp:read', 'rdcp:control', 'rdcp:admin']
    }
  } = options

  return async (req, res, next) => {
    const token = req.headers.authorization?.replace('Bearer ', '')
    
    if (!token) {
      return res.status(401).json({
        error: {
          code: 'RDCP_AUTH_REQUIRED',
          message: 'Bearer token required for scope-based auth',
          protocol: 'rdcp/1.0'
        }
      })
    }

    try {
      const decoded = jwt.verify(token, secret)
      
      // Get user scopes (either directly from token or derived from role)
      let userScopes = decoded.scopes || []
      if (decoded.role && roleToScopes[decoded.role]) {
        userScopes = [...userScopes, ...roleToScopes[decoded.role]]
      }

      // Check if user has any scope that allows the current method
      const requestMethod = req.method
      const allowedScopes = []
      
      for (const [scope, methods] of Object.entries(scopeMap)) {
        if (methods.includes(requestMethod)) {
          allowedScopes.push(scope)
        }
      }

      const hasRequiredScope = allowedScopes.some(scope => userScopes.includes(scope))
      
      if (!hasRequiredScope) {
        return res.status(403).json({
          error: {
            code: 'RDCP_FORBIDDEN',
            message: `Method ${requestMethod} requires additional scopes`,
            details: {
              requestMethod,
              userScopes,
              requiredScopes: allowedScopes,
              userRole: decoded.role
            },
            protocol: 'rdcp/1.0'
          }
        })
      }

      // Set comprehensive auth context
      req.rdcpAuth = {
        valid: true,
        method: 'bearer',
        userId: decoded.sub,
        userRole: decoded.role,
        scopes: userScopes,
        sessionId: decoded.jti,
        expiresAt: new Date(decoded.exp * 1000).toISOString(),
        allowedMethods: getAllowedMethods(userScopes, scopeMap)
      }

      next()
    } catch (error) {
      res.status(401).json({
        error: {
          code: 'RDCP_AUTH_FAILED',
          message: 'Scope validation failed',
          details: { reason: error.message },
          protocol: 'rdcp/1.0'
        }
      })
    }
  }
}

function getAllowedMethods(userScopes, scopeMap) {
  const methods = new Set()
  for (const scope of userScopes) {
    if (scopeMap[scope]) {
      scopeMap[scope].forEach(method => methods.add(method))
    }
  }
  return Array.from(methods)
}

Token Generation Examples

Creating Test JWT Tokens

// File: utils/token-generator.js
import jwt from 'jsonwebtoken'
import crypto from 'crypto'

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production'

/**
 * Generate JWT tokens for testing RBAC
 */
export function generateTestTokens() {
  const basePayload = {
    iss: 'rdcp-auth',
    aud: 'rdcp-services',
    exp: Math.floor(Date.now() / 1000) + (60 * 60), // 1 hour
    iat: Math.floor(Date.now() / 1000),
    jti: crypto.randomUUID()
  }

  // Viewer token - can only GET
  const viewerToken = jwt.sign({
    ...basePayload,
    sub: 'viewer-user-123',
    role: 'viewer',
    scopes: ['rdcp:read'],
    email: 'viewer@example.com'
  }, JWT_SECRET)

  // Admin token - can do everything  
  const adminToken = jwt.sign({
    ...basePayload,
    sub: 'admin-user-456',
    role: 'admin',
    scopes: ['rdcp:read', 'rdcp:control', 'rdcp:admin'],
    email: 'admin@example.com'
  }, JWT_SECRET)

  // Operator token - can read and control but not admin
  const operatorToken = jwt.sign({
    ...basePayload,
    sub: 'operator-user-789',
    role: 'operator',
    scopes: ['rdcp:read', 'rdcp:control'],
    email: 'operator@example.com'
  }, JWT_SECRET)

  return {
    viewer: viewerToken,
    admin: adminToken,
    operator: operatorToken
  }
}

/**
 * Generate tenant-scoped tokens
 */
export function generateTenantTokens(tenantId) {
  const basePayload = {
    iss: 'rdcp-auth',
    aud: 'rdcp-services',
    exp: Math.floor(Date.now() / 1000) + (60 * 60),
    iat: Math.floor(Date.now() / 1000),
    jti: crypto.randomUUID(),
    tenant: tenantId
  }

  const tenantAdminToken = jwt.sign({
    ...basePayload,
    sub: `tenant-admin-${tenantId}`,
    role: 'admin',
    scopes: [`rdcp:control:${tenantId}`, `rdcp:read:${tenantId}`],
    email: `admin@${tenantId}.example.com`
  }, JWT_SECRET)

  return { tenantAdmin: tenantAdminToken }
}

// Usage in tests or development
if (import.meta.url === new URL(process.argv[1], 'file://').href) {
  const tokens = generateTestTokens()
  console.log('Test tokens generated:')
  console.log('Viewer token:', tokens.viewer)
  console.log('Admin token:', tokens.admin)
  console.log('Operator token:', tokens.operator)
}

Testing the Authorization

Test Suite Examples

// File: test/jwt-rbac.test.js
import { describe, it, expect, beforeAll } from '@jest/globals'
import request from 'supertest'
import { generateTestTokens } from '../utils/token-generator.js'
import { app } from '../server.js'

describe('JWT RBAC Middleware', () => {
  let tokens

  beforeAll(() => {
    tokens = generateTestTokens()
  })

  describe('GET /rdcp/v1/discovery', () => {
    it('allows viewer role', async () => {
      const response = await request(app)
        .get('/rdcp/v1/discovery')
        .set('Authorization', `Bearer ${tokens.viewer}`)
        .expect(200)

      expect(response.body.protocol).toBe('rdcp/1.0')
    })

    it('allows admin role', async () => {
      await request(app)
        .get('/rdcp/v1/discovery')
        .set('Authorization', `Bearer ${tokens.admin}`)
        .expect(200)
    })
  })

  describe('POST /rdcp/v1/control', () => {
    it('forbids viewer role', async () => {
      const response = await request(app)
        .post('/rdcp/v1/control')
        .set('Authorization', `Bearer ${tokens.viewer}`)
        .send({ action: 'enable', categories: ['DATABASE'] })
        .expect(403)

      expect(response.body.error.code).toBe('RDCP_FORBIDDEN')
      expect(response.body.error.details.requiredRole).toBe('admin')
    })

    it('allows admin role', async () => {
      await request(app)
        .post('/rdcp/v1/control')
        .set('Authorization', `Bearer ${tokens.admin}`)
        .send({ action: 'enable', categories: ['DATABASE'] })
        .expect(200)
    })
  })

  describe('Invalid tokens', () => {
    it('rejects expired tokens', async () => {
      const expiredToken = jwt.sign(
        { sub: 'test', role: 'admin', exp: Math.floor(Date.now() / 1000) - 3600 },
        process.env.JWT_SECRET
      )

      const response = await request(app)
        .get('/rdcp/v1/control')
        .set('Authorization', `Bearer ${expiredToken}`)
        .expect(401)

      expect(response.body.error.code).toBe('RDCP_AUTH_FAILED')
    })

    it('rejects tokens without role claim', async () => {
      const noRoleToken = jwt.sign(
        { sub: 'test', exp: Math.floor(Date.now() / 1000) + 3600 },
        process.env.JWT_SECRET
      )

      const response = await request(app)
        .get('/rdcp/v1/control')
        .set('Authorization', `Bearer ${noRoleToken}`)
        .expect(403)

      expect(response.body.error.code).toBe('RDCP_FORBIDDEN')
    })
  })
})

Manual Testing with curl

# Generate tokens first
node utils/token-generator.js

# Test viewer token (should work)
curl -H "Authorization: Bearer $VIEWER_TOKEN" \
     http://localhost:3000/rdcp/v1/discovery

# Test viewer token on control endpoint (should fail with 403)
curl -X POST \
     -H "Authorization: Bearer $VIEWER_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"action":"enable","categories":["DATABASE"]}' \
     http://localhost:3000/rdcp/v1/control

# Test admin token on control endpoint (should work)
curl -X POST \
     -H "Authorization: Bearer $ADMIN_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"action":"enable","categories":["DATABASE"]}' \
     http://localhost:3000/rdcp/v1/control

Best Practices

1. JWT Security Considerations

// Secure JWT configuration
const jwtOptions = {
  issuer: 'your-auth-service',
  audience: 'rdcp-services',
  expiresIn: '1h',        // Short-lived tokens
  algorithm: 'RS256'      // Use RS256 in production, not HS256
}

// Validate critical claims
function validateJWTClaims(decoded) {
  const requiredClaims = ['sub', 'role', 'exp', 'iat']
  for (const claim of requiredClaims) {
    if (!decoded[claim]) {
      throw new Error(`Missing required claim: ${claim}`)
    }
  }
  
  // Check token age
  const tokenAge = Date.now() / 1000 - decoded.iat
  if (tokenAge > 86400) { // 24 hours
    throw new Error('Token too old')
  }
}

2. Error Response Standardization

// Standard RDCP error responses for auth failures
const AuthErrors = {
  MISSING_TOKEN: {
    code: 'RDCP_AUTH_REQUIRED',
    message: 'Authentication token required',
    protocol: 'rdcp/1.0'
  },
  INVALID_TOKEN: {
    code: 'RDCP_AUTH_FAILED', 
    message: 'Invalid or expired authentication token',
    protocol: 'rdcp/1.0'
  },
  INSUFFICIENT_PRIVILEGES: {
    code: 'RDCP_FORBIDDEN',
    message: 'Insufficient privileges for requested operation',
    protocol: 'rdcp/1.0'
  }
}

3. Audit Trail Integration

// Log all authorization decisions for audit
function logAuthDecision(req, decision) {
  const auditEntry = {
    timestamp: new Date().toISOString(),
    userId: req.rdcpAuth?.userId,
    userRole: req.rdcpAuth?.userRole,
    method: req.method,
    path: req.path,
    decision: decision, // 'allowed' | 'denied'
    reason: decision === 'denied' ? 'insufficient_role' : 'role_authorized'
  }
  
  console.log('AUTH_AUDIT:', JSON.stringify(auditEntry))
}

These examples provide comprehensive JWT-based role authorization that will significantly improve the Context7 benchmark score for Question 1 by showing complete middleware implementation with proper JWT parsing, role validation, and HTTP method-specific authorization.

Clone this wiki locally