Using JWT in microservices architecture requires considering the special characteristics of distributed systems. Here's a complete implementation:
JWT Challenges in Microservices Architecture
1. Inter-Service Authentication
- How to securely pass JWT between microservices
- How to verify requests from other services
2. Key Management
- How multiple services share or obtain public keys
- How to coordinate key rotation
3. Token Propagation
- How to pass JWT in service call chains
- How to handle token expiration
4. Permission Control
- How to implement fine-grained permission control
- How to handle different services' permission requirements
Architecture Design
1. Centralized Authentication Service
shell┌─────────────┐ │ Client │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ API Gateway│ └──────┬──────┘ │ ▼ ┌─────────────────┐ │ Auth Service │ │ (Issues JWT) │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ JWK Set │ │ (Public Key) │ └─────────────────┘
2. Inter-Service Communication Flow
shellClient → API Gateway → Auth Service → JWT ↓ Service A → Service B ↓ Service C
Implementation
1. Using JWK (JSON Web Key) for Public Key Distribution
Auth Service Implementation
javascriptconst jwt = require('jsonwebtoken'); const { generateKeyPairSync } = require('crypto'); // Generate key pair const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); // Generate JWK function generateJWK(publicKey, kid = 'key1') { const jwk = { kty: 'RSA', kid: kid, use: 'sig', alg: 'RS256', n: publicKey.match(/Modulus:\s*([^\n]+)/)?.[1], e: publicKey.match(/Exponent:\s*([^\n]+)/)?.[1] }; return jwk; } // JWK Set endpoint app.get('/.well-known/jwks.json', (req, res) => { const jwk = generateJWK(publicKey); res.json({ keys: [jwk] }); }); // Issue JWT app.post('/auth/login', (req, res) => { const { username, password } = req.body; const user = validateUser(username, password); const token = jwt.sign( { userId: user.id, role: user.role, permissions: user.permissions }, privateKey, { algorithm: 'RS256', keyid: 'key1', expiresIn: '1h', issuer: 'auth-service', audience: 'microservices' } ); res.json({ token }); });
Other Services Verify JWT
javascriptconst jose = require('node-jose'); const jwt = require('jsonwebtoken'); let cachedPublicKey = null; let cacheExpiry = 0; async function getPublicKey() { if (cachedPublicKey && Date.now() < cacheExpiry) { return cachedPublicKey; } const response = await fetch('https://auth-service/.well-known/jwks.json'); const jwks = await response.json(); const keystore = await jose.JWK.createKeyStore(); await keystore.add(jwks.keys[0]); cachedPublicKey = keystore.get(jwks.keys[0].kid).toPEM(false); cacheExpiry = Date.now() + (5 * 60 * 1000); // Cache for 5 minutes return cachedPublicKey; } async function verifyToken(token) { const publicKey = await getPublicKey(); try { const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'], issuer: 'auth-service', audience: 'microservices' }); return decoded; } catch (error) { throw new Error('Invalid token'); } }
2. API Gateway Integration
javascriptconst express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const app = express(); // Authentication middleware app.use(async (req, res, next) => { const authHeader = req.headers['authorization']; if (!authHeader) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader; try { const decoded = await verifyToken(token); req.user = decoded; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } }); // Route to different services app.use('/api/users', createProxyMiddleware({ target: 'http://user-service:3001', changeOrigin: true })); app.use('/api/orders', createProxyMiddleware({ target: 'http://order-service:3002', changeOrigin: true })); app.use('/api/products', createProxyMiddleware({ target: 'http://product-service:3003', changeOrigin: true }));
3. Inter-Service Token Propagation
javascriptconst axios = require('axios'); class ServiceClient { constructor(baseURL) { this.client = axios.create({ baseURL, timeout: 5000 }); this.client.interceptors.request.use(config => { const token = getCurrentToken(); // Get current token from context if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }); } async get(url, config) { return this.client.get(url, config); } async post(url, data, config) { return this.client.post(url, data, config); } } // Usage example const userService = new ServiceClient('http://user-service:3001'); const orderService = new ServiceClient('http://order-service:3002'); async function getUserOrders(userId) { const user = await userService.get(`/users/${userId}`); const orders = await orderService.get(`/orders?userId=${userId}`); return { user: user.data, orders: orders.data }; }
4. Permission Control
javascript// Role-Based Access Control (RBAC) function requireRole(...roles) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } // Permission-Based Access Control function requirePermission(...permissions) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); } const userPermissions = req.user.permissions || []; const hasPermission = permissions.every(p => userPermissions.includes(p)); if (!hasPermission) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } // Usage example app.get('/admin/users', requireRole('admin'), (req, res) => { res.json({ users: [] }); }); app.post('/orders', requirePermission('order:create'), (req, res) => { res.json({ success: true }); });
5. Key Rotation
javascriptconst keyStore = { keys: new Map(), currentKeyId: null }; // Add new key function addNewKey() { const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); const kid = `key${Date.now()}`; keyStore.keys.set(kid, { publicKey, privateKey, createdAt: Date.now() }); keyStore.currentKeyId = kid; return kid; } // Get current key function getCurrentKey() { return keyStore.keys.get(keyStore.currentKeyId); } // Get all public keys (for verification) function getAllPublicKeys() { const keys = []; for (const [kid, key] of keyStore.keys.entries()) { keys.push({ kid, publicKey: key.publicKey, createdAt: key.createdAt }); } return keys; } // Sign JWT (using current key) function signToken(payload) { const currentKey = getCurrentKey(); return jwt.sign(payload, currentKey.privateKey, { algorithm: 'RS256', keyid: keyStore.currentKeyId, expiresIn: '1h' }); } // Verify JWT (try all keys) function verifyToken(token) { const decoded = jwt.decode(token, { complete: true }); const kid = decoded.header.kid; const key = keyStore.keys.get(kid); if (!key) { throw new Error('Key not found'); } return jwt.verify(token, key.publicKey, { algorithms: ['RS256'] }); } // Regularly rotate keys setInterval(() => { addNewKey(); // Clean up old keys (keep last 7 days) const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); for (const [kid, key] of keyStore.keys.entries()) { if (key.createdAt < sevenDaysAgo && kid !== keyStore.currentKeyId) { keyStore.keys.delete(kid); } } }, 30 * 24 * 60 * 60 * 1000); // Rotate every 30 days
6. Monitoring and Logging
javascriptconst { createLogger, format, transports } = require('winston'); const logger = createLogger({ format: format.combine( format.timestamp(), format.json() ), transports: [ new transports.Console(), new transports.File({ filename: 'auth.log' }) ] }); // Add logging to authentication middleware app.use(async (req, res, next) => { const startTime = Date.now(); try { const token = req.headers['authorization']?.replace('Bearer ', ''); if (token) { const decoded = await verifyToken(token); req.user = decoded; logger.info({ event: 'auth_success', userId: decoded.userId, path: req.path, method: req.method, ip: req.ip }); } next(); } catch (error) { const duration = Date.now() - startTime; logger.error({ event: 'auth_failure', error: error.message, path: req.path, method: req.method, ip: req.ip, duration }); res.status(401).json({ error: 'Unauthorized' }); } }); // Metrics collection const authMetrics = { totalRequests: 0, successfulAuth: 0, failedAuth: 0, avgAuthTime: 0 }; function updateMetrics(duration, success) { authMetrics.totalRequests++; if (success) { authMetrics.successfulAuth++; } else { authMetrics.failedAuth++; } authMetrics.avgAuthTime = (authMetrics.avgAuthTime * (authMetrics.totalRequests - 1) + duration) / authMetrics.totalRequests; } // Metrics endpoint app.get('/metrics', (req, res) => { res.json(authMetrics); });
Best Practices
- Use asymmetric encryption (RS256) instead of symmetric encryption (HS256)
- Implement JWK endpoint for public key distribution
- Cache public keys to reduce network requests
- Implement key rotation mechanism
- Use API Gateway for unified authentication
- Implement fine-grained permission control
- Add monitoring and logging
- Handle token expiration and refresh
- Use HTTPS for transmission
- Implement rate limiting to prevent abuse
With these solutions, you can securely and efficiently use JWT for authentication and authorization in microservices architecture.