← All Guides

JWT Best Practices for Microservices

Comprehensive guide to implementing secure and scalable JWT authentication in microservices architecture, covering token propagation, validation strategies, and distributed security.

Microservices Authentication Challenges

Microservices introduce unique authentication challenges that traditional monolithic applications don't face. JWT tokens provide an elegant solution, but proper implementation is critical for security and scalability.

Common Microservices Auth Problems

  • Token propagation: How to pass authentication context between services
  • Distributed validation: Each service validating tokens independently
  • Key management: Distributing signing keys securely across services
  • Token refresh: Handling expiration in service-to-service calls
  • Authorization decisions: Fine-grained permissions across services

Why JWT for Microservices: JWTs are stateless, self-contained, and can be validated independently by each service without database lookups, making them ideal for distributed architectures.

Token Types in Microservices

Access Tokens vs Service Tokens

User Access Tokens

  • Purpose: User authentication
  • Lifetime: 15-60 minutes
  • Contains: User ID, roles, permissions
  • Propagated: Through request headers

Service-to-Service Tokens

  • Purpose: Service authentication
  • Lifetime: 5-15 minutes
  • Contains: Service ID, scopes
  • Propagated: Automatically by service mesh

Token Structure for Microservices

// User Access Token Example
{
  "iss": "https://auth.company.com",
  "aud": ["api.company.com", "billing.company.com"],
  "sub": "user123",
  "iat": 1640995200,
  "exp": 1640998800,
  "scope": "read:profile write:orders",
  "roles": ["user", "premium"],
  "permissions": ["orders:create", "profile:update"],
  "tenant_id": "org456"
}

// Service Token Example
{
  "iss": "https://auth.company.com",
  "aud": "internal.company.com",
  "sub": "service:order-processor",
  "iat": 1640995200,
  "exp": 1640996100,
  "scope": "payments:charge inventory:reserve",
  "service_version": "v1.2.3"
}

Token Propagation Strategies

1. Manual Header Propagation

Each service manually forwards the JWT to downstream services:

// Node.js/Express example
async function callDownstreamService(userToken, data) {
  const response = await fetch('https://billing-service/api/charge', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${userToken}`,
      'Content-Type': 'application/json',
      'X-Service-Name': 'order-service',
      'X-Request-ID': generateRequestId()
    },
    body: JSON.stringify(data)
  });
  
  if (!response.ok) {
    throw new Error(`Billing service error: ${response.status}`);
  }
  
  return response.json();
}

2. Service Mesh Integration

Use a service mesh like Istio or Linkerd for automatic token propagation:

# Istio AuthorizationPolicy
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: jwt-propagation
spec:
  rules:
  - when:
    - key: request.headers[authorization]
      values: ["Bearer *"]
    to:
    - operation:
        methods: ["*"]
  - action: CUSTOM
    provider:
      name: jwt-propagator
    rules:
    - to:
      - operation:
          methods: ["*"]

3. Gateway-Level Token Management

// API Gateway middleware
class JWTGatewayMiddleware {
  async handleRequest(req, res, next) {
    try {
      // 1. Validate incoming user token
      const userToken = this.extractToken(req);
      const userClaims = await this.validateUserToken(userToken);
      
      // 2. Generate service token for downstream calls
      const serviceToken = await this.generateServiceToken({
        user_id: userClaims.sub,
        user_roles: userClaims.roles,
        original_token: userToken,
        service_chain: req.headers['x-service-chain'] || []
      });
      
      // 3. Add tokens to request context
      req.user = userClaims;
      req.serviceToken = serviceToken;
      req.headers['x-service-token'] = serviceToken;
      
      next();
    } catch (error) {
      res.status(401).json({ error: 'Authentication failed' });
    }
  }
}

Distributed Validation Strategies

1. Shared Secret Validation

All services share the same HMAC secret (suitable for small, trusted environments):

// Shared secret approach
const jwt = require('jsonwebtoken');

class SharedSecretValidator {
  constructor(secret) {
    this.secret = secret;
  }
  
  validateToken(token) {
    return jwt.verify(token, this.secret, {
      algorithms: ['HS256'],
      issuer: process.env.JWT_ISSUER,
      audience: process.env.SERVICE_NAME
    });
  }
}

// Use in microservice
const validator = new SharedSecretValidator(process.env.JWT_SECRET);

app.use('/api/*', (req, res, next) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    req.user = validator.validateToken(token);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

2. Public Key Validation (JWKS)

Services fetch public keys from a centralized JWKS endpoint:

const jwksClient = require('jwks-rsa');

class JWKSValidator {
  constructor(jwksUri) {
    this.client = jwksClient({
      jwksUri,
      requestHeaders: {}, 
      timeout: 30000,
      jwksRequestsPerMinute: 5,
      cache: true,
      cacheMaxEntries: 5,
      cacheMaxAge: 600000 // 10 minutes
    });
  }
  
  async getSigningKey(kid) {
    const key = await this.client.getSigningKey(kid);
    return key.getPublicKey();
  }
  
  async validateToken(token) {
    const decoded = jwt.decode(token, { complete: true });
    
    if (!decoded || !decoded.header.kid) {
      throw new Error('Invalid token format');
    }
    
    const publicKey = await this.getSigningKey(decoded.header.kid);
    
    return jwt.verify(token, publicKey, {
      algorithms: ['RS256', 'ES256'],
      issuer: process.env.JWT_ISSUER,
      audience: process.env.SERVICE_NAME
    });
  }
}

// Auto-refresh keys periodically
setInterval(async () => {
  try {
    await validator.client.getKeys();
  } catch (error) {
    console.error('Failed to refresh JWKS:', error);
  }
}, 300000); // 5 minutes

3. Cached Validation

const NodeCache = require('node-cache');

class CachedJWTValidator {
  constructor(jwksValidator) {
    this.jwksValidator = jwksValidator;
    this.cache = new NodeCache({ 
      stdTTL: 300, // 5 minutes
      checkperiod: 60 // Check for expired keys every minute
    });
  }
  
  async validateToken(token) {
    // Create a cache key from token signature
    const tokenParts = token.split('.');
    const signatureHash = crypto
      .createHash('sha256')
      .update(tokenParts[2])
      .digest('hex');
    
    const cacheKey = `jwt:${signatureHash}`;
    
    // Check cache first
    let payload = this.cache.get(cacheKey);
    if (payload) {
      // Still need to check expiration
      if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
        this.cache.del(cacheKey);
        throw new Error('Token expired');
      }
      return payload;
    }
    
    // Validate and cache
    payload = await this.jwksValidator.validateToken(token);
    this.cache.set(cacheKey, payload);
    
    return payload;
  }
}

Key Management in Microservices

Centralized Key Management

Use a centralized key management service for rotating keys:

class KeyManagementService {
  constructor(keyServiceUrl, apiKey) {
    this.keyServiceUrl = keyServiceUrl;
    this.apiKey = apiKey;
    this.keyCache = new Map();
    this.refreshInterval = null;
  }
  
  async start() {
    await this.refreshKeys();
    
    // Refresh keys every 5 minutes
    this.refreshInterval = setInterval(() => {
      this.refreshKeys().catch(console.error);
    }, 300000);
  }
  
  async refreshKeys() {
    try {
      const response = await fetch(`${this.keyServiceUrl}/keys`, {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
          'X-Service-Name': process.env.SERVICE_NAME
        }
      });
      
      const keys = await response.json();
      
      // Update cache with new keys
      for (const key of keys) {
        this.keyCache.set(key.kid, {
          publicKey: key.x5c ? this.parseX5C(key.x5c[0]) : key.n,
          algorithm: key.alg,
          validFrom: new Date(key.nbf * 1000),
          validUntil: new Date(key.exp * 1000)
        });
      }
      
      // Remove expired keys
      for (const [kid, keyInfo] of this.keyCache) {
        if (keyInfo.validUntil < new Date()) {
          this.keyCache.delete(kid);
        }
      }
      
      console.log(`Refreshed ${keys.length} keys`);
    } catch (error) {
      console.error('Key refresh failed:', error);
    }
  }
  
  getKey(kid) {
    const keyInfo = this.keyCache.get(kid);
    if (!keyInfo || keyInfo.validUntil < new Date()) {
      return null;
    }
    return keyInfo;
  }
}

Environment-Specific Key Rotation

# Kubernetes Secret with automatic rotation
apiVersion: v1
kind: Secret
metadata:
  name: jwt-keys
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "jwt-key-reader"
    vault.hashicorp.com/agent-inject-secret-key: "jwt/current"
    vault.hashicorp.com/agent-inject-template-key: |
      {{- with secret "jwt/current" -}}
      {{ .Data.data.private_key }}
      {{- end -}}
type: Opaque
data:
  private-key: "{{ vault_secret }}"
  public-key: "{{ vault_public_key }}"

---
# CronJob for key rotation
apiVersion: batch/v1
kind: CronJob
metadata:
  name: rotate-jwt-keys
spec:
  schedule: "0 2 * * 0"  # Weekly on Sunday 2 AM
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: key-rotator
            image: vault:latest
            command:
            - /bin/sh
            - -c
            - |
              # Generate new key pair
              vault write jwt/rotate
              
              # Update all services
              kubectl rollout restart deployment -l app.type=microservice

Service-to-Service Authentication

OAuth2 Client Credentials Flow

class ServiceAuthenticator {
  constructor(clientId, clientSecret, tokenEndpoint) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.tokenEndpoint = tokenEndpoint;
    this.tokenCache = null;
    this.tokenExpiry = 0;
  }
  
  async getServiceToken(scopes = []) {
    // Check if cached token is still valid
    if (this.tokenCache && Date.now() < this.tokenExpiry) {
      return this.tokenCache;
    }
    
    // Request new token
    const response = await fetch(this.tokenEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`
      },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        scope: scopes.join(' ')
      })
    });
    
    const tokenData = await response.json();
    
    // Cache token with safety margin
    this.tokenCache = tokenData.access_token;
    this.tokenExpiry = Date.now() + (tokenData.expires_in - 60) * 1000;
    
    return this.tokenCache;
  }
  
  async authenticatedFetch(url, options = {}, requiredScopes = []) {
    const token = await this.getServiceToken(requiredScopes);
    
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${token}`,
        'X-Service-Name': process.env.SERVICE_NAME,
        'X-Service-Version': process.env.SERVICE_VERSION
      }
    });
  }
}

// Usage in service
const serviceAuth = new ServiceAuthenticator(
  process.env.SERVICE_CLIENT_ID,
  process.env.SERVICE_CLIENT_SECRET,
  process.env.AUTH_TOKEN_ENDPOINT
);

// Call another service
const billingResponse = await serviceAuth.authenticatedFetch(
  'https://billing-service/api/charge',
  {
    method: 'POST',
    body: JSON.stringify(chargeData)
  },
  ['billing:charge']
);

mTLS with JWT

const https = require('https');
const fs = require('fs');

class MTLSJWTClient {
  constructor(cert, key, ca) {
    this.httpsAgent = new https.Agent({
      cert: fs.readFileSync(cert),
      key: fs.readFileSync(key),
      ca: fs.readFileSync(ca),
      rejectUnauthorized: true
    });
  }
  
  async callService(url, token, data) {
    const response = await fetch(url, {
      method: 'POST',
      agent: this.httpsAgent,
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
        'X-Client-Cert-Subject': this.getCertSubject()
      },
      body: JSON.stringify(data)
    });
    
    return response.json();
  }
  
  getCertSubject() {
    // Extract subject from client certificate
    const cert = this.httpsAgent.options.cert;
    // Implementation depends on cert format
    return 'CN=order-service,O=Company,C=US';
  }
}

Authorization in Microservices

Fine-Grained Permissions

class MicroserviceAuthorizer {
  constructor(serviceName) {
    this.serviceName = serviceName;
    this.permissionMap = {
      'orders:create': ['user', 'admin'],
      'orders:read': ['user', 'admin', 'support'],
      'orders:update': ['admin', 'order-service'],
      'orders:delete': ['admin'],
      'payments:charge': ['order-service', 'billing-service'],
      'inventory:reserve': ['order-service', 'inventory-service']
    };
  }
  
  authorize(token, requiredPermission) {
    const claims = jwt.verify(token, this.getValidationKey());
    
    // Check direct permission
    if (claims.permissions?.includes(requiredPermission)) {
      return true;
    }
    
    // Check role-based permission
    const allowedRoles = this.permissionMap[requiredPermission] || [];
    const userRoles = claims.roles || [];
    
    if (userRoles.some(role => allowedRoles.includes(role))) {
      return true;
    }
    
    // Check service-to-service permission
    if (claims.sub?.startsWith('service:')) {
      const serviceName = claims.sub.split(':')[1];
      return allowedRoles.includes(serviceName);
    }
    
    return false;
  }
  
  middleware(requiredPermission) {
    return (req, res, next) => {
      try {
        const token = req.headers.authorization?.split(' ')[1];
        if (!token) {
          return res.status(401).json({ error: 'No token provided' });
        }
        
        if (this.authorize(token, requiredPermission)) {
          next();
        } else {
          res.status(403).json({ 
            error: 'Insufficient permissions',
            required: requiredPermission 
          });
        }
      } catch (error) {
        res.status(401).json({ error: 'Invalid token' });
      }
    };
  }
}

// Usage
const authorizer = new MicroserviceAuthorizer('order-service');

app.post('/api/orders', 
  authorizer.middleware('orders:create'),
  async (req, res) => {
    // Order creation logic
  }
);

Context-Aware Authorization

class ContextualAuthorizer extends MicroserviceAuthorizer {
  async authorizeWithContext(token, permission, context = {}) {
    const baseAuthorized = this.authorize(token, permission);
    if (!baseAuthorized) {
      return false;
    }
    
    const claims = jwt.verify(token, this.getValidationKey());
    
    // Tenant isolation
    if (context.tenantId && claims.tenant_id !== context.tenantId) {
      return false;
    }
    
    // Resource ownership
    if (context.resourceOwnerId && 
        claims.sub !== context.resourceOwnerId &&
        !claims.roles?.includes('admin')) {
      return false;
    }
    
    // Time-based restrictions
    if (context.businessHoursOnly && !this.isBusinessHours()) {
      return claims.roles?.includes('admin') || false;
    }
    
    // Location-based restrictions
    if (context.allowedRegions && 
        !context.allowedRegions.includes(claims.region)) {
      return false;
    }
    
    return true;
  }
  
  isBusinessHours() {
    const now = new Date();
    const hour = now.getHours();
    const day = now.getDay();
    
    // Monday-Friday, 9 AM - 6 PM
    return day >= 1 && day <= 5 && hour >= 9 && hour < 18;
  }
}

Error Handling & Observability

Distributed Tracing with JWT

const opentelemetry = require('@opentelemetry/api');

class TracingJWTMiddleware {
  constructor(validator) {
    this.validator = validator;
    this.tracer = opentelemetry.trace.getTracer('jwt-auth');
  }
  
  middleware() {
    return async (req, res, next) => {
      const span = this.tracer.startSpan('jwt.validation');
      
      try {
        const token = req.headers.authorization?.split(' ')[1];
        
        // Add token info to span (without sensitive data)
        if (token) {
          const decoded = jwt.decode(token, { complete: true });
          span.setAttributes({
            'jwt.algorithm': decoded?.header.alg,
            'jwt.issuer': decoded?.payload.iss,
            'jwt.subject': decoded?.payload.sub?.substring(0, 8) + '...',
            'jwt.audience': Array.isArray(decoded?.payload.aud) 
              ? decoded.payload.aud.join(',') 
              : decoded?.payload.aud
          });
        }
        
        const payload = await this.validator.validateToken(token);
        
        span.setAttributes({
          'jwt.valid': true,
          'jwt.expires_at': payload.exp,
          'user.id': payload.sub,
          'user.roles': payload.roles?.join(',') || ''
        });
        
        req.user = payload;
        req.span = span;
        next();
        
      } catch (error) {
        span.recordException(error);
        span.setAttributes({
          'jwt.valid': false,
          'jwt.error': error.message
        });
        
        res.status(401).json({ 
          error: 'Authentication failed',
          trace_id: span.spanContext().traceId
        });
      } finally {
        span.end();
      }
    };
  }
}

Security Monitoring

class JWTSecurityMonitor {
  constructor(alertingService) {
    this.alertingService = alertingService;
    this.suspiciousPatterns = {
      rapid_failures: { threshold: 10, window: 60000 }, // 10 failures in 1 min
      algorithm_confusion: { threshold: 5, window: 300000 }, // 5 attempts in 5 min
      expired_token_reuse: { threshold: 3, window: 600000 } // 3 attempts in 10 min
    };
    this.eventCounts = new Map();
  }
  
  recordEvent(type, metadata = {}) {
    const key = `${type}:${metadata.ip || 'unknown'}`;
    const now = Date.now();
    
    if (!this.eventCounts.has(key)) {
      this.eventCounts.set(key, []);
    }
    
    const events = this.eventCounts.get(key);
    events.push({ timestamp: now, metadata });
    
    // Clean old events
    const pattern = this.suspiciousPatterns[type];
    if (pattern) {
      const cutoff = now - pattern.window;
      const recentEvents = events.filter(e => e.timestamp > cutoff);
      this.eventCounts.set(key, recentEvents);
      
      // Check threshold
      if (recentEvents.length >= pattern.threshold) {
        this.alertingService.sendAlert({
          type: 'jwt_security_incident',
          pattern: type,
          count: recentEvents.length,
          window: pattern.window,
          source_ip: metadata.ip,
          details: recentEvents
        });
      }
    }
  }
  
  monitorValidation(req, error = null) {
    if (error) {
      if (error.name === 'TokenExpiredError') {
        this.recordEvent('expired_token_reuse', { 
          ip: req.ip, 
          user_agent: req.headers['user-agent'] 
        });
      } else if (error.message.includes('algorithm')) {
        this.recordEvent('algorithm_confusion', { 
          ip: req.ip,
          algorithm: error.algorithm
        });
      } else {
        this.recordEvent('rapid_failures', { ip: req.ip });
      }
    }
  }
}

Performance Optimization

Connection Pooling & Caching

class OptimizedJWTValidator {
  constructor() {
    this.jwksClient = jwksClient({
      jwksUri: process.env.JWKS_URI,
      cache: true,
      cacheMaxEntries: 50,
      cacheMaxAge: 600000, // 10 minutes
      rateLimit: true,
      jwksRequestsPerMinute: 10
    });
    
    // Redis for blacklist caching
    this.redis = new Redis({
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      connectTimeout: 1000,
      lazyConnect: true,
      maxRetriesPerRequest: 3
    });
    
    // Local LRU cache for validated tokens
    this.tokenCache = new LRU({
      max: 1000,
      maxAge: 300000 // 5 minutes
    });
  }
  
  async validateToken(token) {
    // 1. Check local cache first
    const cached = this.tokenCache.get(token);
    if (cached) {
      return cached;
    }
    
    // 2. Parse token
    const decoded = jwt.decode(token, { complete: true });
    if (!decoded) {
      throw new Error('Invalid token format');
    }
    
    // 3. Check blacklist (with fallback)
    try {
      const blacklisted = await this.redis.get(`bl:${decoded.payload.jti}`);
      if (blacklisted) {
        throw new Error('Token has been revoked');
      }
    } catch (redisError) {
      console.warn('Redis blacklist check failed:', redisError.message);
      // Continue validation - don't fail on Redis errors
    }
    
    // 4. Verify signature
    const publicKey = await this.jwksClient.getSigningKey(decoded.header.kid);
    const payload = jwt.verify(token, publicKey.getPublicKey(), {
      algorithms: ['RS256', 'ES256'],
      issuer: process.env.JWT_ISSUER,
      audience: process.env.SERVICE_NAME
    });
    
    // 5. Cache result
    this.tokenCache.set(token, payload);
    
    return payload;
  }
  
  // Batch validate multiple tokens
  async validateTokens(tokens) {
    const results = await Promise.allSettled(
      tokens.map(token => this.validateToken(token))
    );
    
    return results.map((result, index) => ({
      token: tokens[index],
      valid: result.status === 'fulfilled',
      payload: result.status === 'fulfilled' ? result.value : null,
      error: result.status === 'rejected' ? result.reason.message : null
    }));
  }
}

Async Validation

class AsyncJWTProcessor {
  constructor(validator) {
    this.validator = validator;
    this.queue = new Queue('jwt-validation', {
      redis: process.env.REDIS_URL
    });
    
    this.setupWorker();
  }
  
  setupWorker() {
    this.queue.process('validate', async (job) => {
      const { token, requestId } = job.data;
      
      try {
        const payload = await this.validator.validateToken(token);
        
        // Store result for pickup
        await this.redis.setex(
          `result:${requestId}`,
          60, // 1 minute TTL
          JSON.stringify({ valid: true, payload })
        );
        
      } catch (error) {
        await this.redis.setex(
          `result:${requestId}`,
          60,
          JSON.stringify({ valid: false, error: error.message })
        );
      }
    });
  }
  
  // For non-critical paths
  async validateAsync(token) {
    const requestId = uuidv4();
    
    await this.queue.add('validate', { token, requestId });
    
    return requestId;
  }
  
  async getValidationResult(requestId) {
    const result = await this.redis.get(`result:${requestId}`);
    return result ? JSON.parse(result) : null;
  }
}

Deployment & DevOps

Docker Configuration

# Multi-stage build for security
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS runtime
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .

# JWT validation keys
COPY --chown=nextjs:nodejs keys/ ./keys/
RUN chmod 600 ./keys/*

USER nextjs

EXPOSE 3000

# Health check endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
  CMD node health-check.js || exit 1

CMD ["node", "server.js"]

Kubernetes JWT Secrets

apiVersion: v1
kind: ConfigMap
metadata:
  name: jwt-config
data:
  issuer: "https://auth.company.com"
  audience: "api.company.com"
  algorithm: "RS256"
  
---
apiVersion: v1
kind: Secret
metadata:
  name: jwt-keys
type: kubernetes.io/tls
data:
  tls.crt: # Base64 encoded public key
  tls.key: # Base64 encoded private key (for signing services only)
  
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: order-service:latest
        env:
        - name: JWT_ISSUER
          valueFrom:
            configMapKeyRef:
              name: jwt-config
              key: issuer
        - name: JWT_AUDIENCE
          valueFrom:
            configMapKeyRef:
              name: jwt-config
              key: audience
        - name: JWT_PUBLIC_KEY_PATH
          value: "/etc/jwt/tls.crt"
        volumeMounts:
        - name: jwt-keys
          mountPath: "/etc/jwt"
          readOnly: true
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: jwt-keys
        secret:
          secretName: jwt-keys

Testing Strategies

JWT Mock Testing

// Test utilities
class JWTTestUtils {
  static createTestToken(claims = {}, options = {}) {
    const defaultClaims = {
      iss: 'test-issuer',
      aud: 'test-service',
      sub: 'test-user-123',
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 3600,
      roles: ['user'],
      permissions: ['read:profile']
    };
    
    return jwt.sign(
      { ...defaultClaims, ...claims },
      options.secret || 'test-secret',
      { algorithm: options.algorithm || 'HS256' }
    );
  }
  
  static createExpiredToken(claims = {}) {
    return this.createTestToken({
      ...claims,
      exp: Math.floor(Date.now() / 1000) - 3600 // 1 hour ago
    });
  }
  
  static createServiceToken(serviceName, scopes = []) {
    return this.createTestToken({
      sub: `service:${serviceName}`,
      scope: scopes.join(' '),
      aud: 'internal.company.com'
    });
  }
}

// Integration tests
describe('Microservice JWT Authentication', () => {
  let app, request;
  
  beforeEach(() => {
    app = createTestApp();
    request = supertest(app);
  });
  
  it('should accept valid user token', async () => {
    const token = JWTTestUtils.createTestToken({
      sub: 'user123',
      roles: ['premium']
    });
    
    const response = await request
      .get('/api/orders')
      .set('Authorization', `Bearer ${token}`);
    
    expect(response.status).toBe(200);
  });
  
  it('should reject expired token', async () => {
    const token = JWTTestUtils.createExpiredToken();
    
    const response = await request
      .get('/api/orders')
      .set('Authorization', `Bearer ${token}`);
    
    expect(response.status).toBe(401);
    expect(response.body.error).toContain('expired');
  });
  
  it('should handle service-to-service calls', async () => {
    const serviceToken = JWTTestUtils.createServiceToken('billing-service', [
      'orders:read'
    ]);
    
    const response = await request
      .get('/api/orders/internal')
      .set('Authorization', `Bearer ${serviceToken}`);
    
    expect(response.status).toBe(200);
  });
});

Security Checklist

Microservices JWT Security Checklist

  • [ ] Use different keys for different environments (dev/staging/prod)
  • [ ] Implement proper key rotation strategy
  • [ ] Validate tokens in every service independently
  • [ ] Use appropriate token lifetimes (short for access, longer for service tokens)
  • [ ] Implement proper error handling without information leakage
  • [ ] Add distributed tracing for auth flows
  • [ ] Monitor for suspicious authentication patterns
  • [ ] Use mTLS for service-to-service communication in production
  • [ ] Implement token blacklist/revocation mechanism
  • [ ] Cache validation results with appropriate TTL
  • [ ] Use service mesh for automatic token propagation
  • [ ] Implement proper audience validation per service
  • [ ] Add circuit breakers for key service dependencies
  • [ ] Test token validation under failure conditions
  • [ ] Implement graceful degradation when auth services are down

Related Tools & Guides