JJWT Decoder
13 min read

JWT in Microservices: Authentication Patterns & Best Practices

Implement JWT authentication in microservices with API gateway patterns, token propagation, and distributed auth strategies.

In This Article

The Authentication Challenge in Microservices

Microservices architectures break monolithic applications into independent, deployable services. This introduces a fundamental authentication challenge: how do you verify a user's identity across dozens of services without creating tight coupling or a single point of failure?

JWT is the most popular solution because it's stateless, self-contained, and can be verified by any service without calling a central authentication server on every request. In this guide, we'll explore the patterns, architectures, and implementation details for using JWT in microservices.

For foundational JWT knowledge, see our JWT security best practices and What is JWT guides.

API Gateway Pattern

The API Gateway is the most common authentication pattern for microservices. All external requests pass through a single gateway that handles authentication and forwards validated requests to downstream services.

How It Works

  1. Client sends request with JWT to the API Gateway
  2. Gateway validates the JWT (signature, expiration, claims)
  3. Gateway forwards the request to the appropriate service, optionally enriching the request with user info
  4. Downstream services trust the gateway and don't re-validate the JWT

Gateway Implementation (Node.js / Express)

const express = require('express');
const jwt = require('jsonwebtoken');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();
const JWKS_URL = 'https://auth.example.com/.well-known/jwks.json';
const jwksClient = require('jwks-rsa');

const client = jwksClient({ jwksUri: JWKS_URL, cache: true, rateLimit: true });

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) return callback(err);
    callback(null, key.getPublicKey());
  });
}

// Authentication middleware on the gateway
function gatewayAuth(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });

  jwt.verify(token, getKey, {
    algorithms: ['RS256'],
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com'
  }, (err, decoded) => {
    if (err) return res.status(401).json({ error: 'Invalid token' });
    // Pass user info to downstream services via headers
    req.headers['x-user-id'] = decoded.sub;
    req.headers['x-user-role'] = decoded.role;
    req.headers['x-user-scopes'] = JSON.stringify(decoded.scope || []);
    next();
  });
}

app.use('/api', gatewayAuth);

// Proxy to user service
app.use('/api/users', createProxyMiddleware({
  target: 'http://user-service:3001',
  pathRewrite: { '^/api/users': '/users' }
}));

// Proxy to order service
app.use('/api/orders', createProxyMiddleware({
  target: 'http://order-service:3002',
  pathRewrite: { '^/api/orders': '/orders' }
}));

app.listen(3000);

Downstream Service (Trusts Gateway)

// user-service/server.js
const express = require('express');
const app = express();

// Trust gateway headers — no JWT verification needed
app.get('/users/:id', (req, res) => {
  const userId = req.headers['x-user-id'];
  const role = req.headers['x-user-role'];

  if (role !== 'admin' && userId !== req.params.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  // Fetch and return user data
  res.json({ id: req.params.id, name: 'John Doe', email: 'john@example.com' });
});

app.listen(3001);

Shared Secret vs Public Key Verification

When multiple services verify JWTs, you have two options:

Shared Secret (HS256)

All services share the same HMAC secret. This is simple but risky:

  • Every service has the signing key — a breach in any service compromises all tokens
  • Key rotation requires updating all services simultaneously
  • Not recommended for production microservices

The auth server signs with a private key, and all services verify with the public key:

  • Only the auth server can sign tokens
  • Public keys are distributed via JWKS endpoints
  • Key rotation is seamless — the JWKS endpoint can host multiple keys
  • Compromised services cannot forge tokens

JWKS-Based Verification

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

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxAge: 600000, // 10 minutes
  rateLimit: true,
  jwksRequestsPerMinute: 10
});

function verifyServiceToken(token) {
  return new Promise((resolve, reject) => {
    jwt.verify(token, (header, callback) => {
      client.getSigningKey(header.kid, (err, key) => {
        if (err) return callback(err);
        callback(null, key.getPublicKey());
      });
    }, {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com',
      audience: 'https://api.example.com'
    }, (err, decoded) => {
      if (err) reject(err);
      else resolve(decoded);
    });
  });
}

Service-to-Service JWT Propagation

When services need to call each other, you need a strategy for propagating the user's identity.

Token Forwarding

The simplest approach: forward the original JWT to the downstream service. This preserves the user's identity and permissions.

// In order-service, calling inventory-service
async function checkInventory(productId, originalToken) {
  const response = await fetch('http://inventory-service:3003/stock', {
    headers: {
      'Authorization': `Bearer ${originalToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ productId })
  });
  return response.json();
}

Token Exchange

For tighter security, exchange the user's token for a service-specific token with limited scope. This follows the principle of least privilege.

async function getServiceToken(originalToken, targetService) {
  const response = await fetch('https://auth.example.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion: originalToken,
      audience: targetService,
      scope: 'read:inventory'
    })
  });
  const data = await response.json();
  return data.access_token;
}

Token Scoping and Authorization

In microservices, JWTs should include scope or permission claims so each service can make authorization decisions independently:

{
  "sub": "user123",
  "scope": ["read:users", "write:orders"],
  "roles": ["customer"],
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com"
}

Each service checks only the scopes relevant to its endpoints:

function requireScope(requiredScope) {
  return (req, res, next) => {
    const userScopes = req.user.scope || [];
    if (!userScopes.includes(requiredScope)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

app.get('/orders', requireScope('read:orders'), getOrders);
app.post('/orders', requireScope('write:orders'), createOrder);

Framework Integrations

Different frameworks offer built-in support for JWT verification in microservices:

  • Spring Boot: Use Spring Security with OAuth2 Resource Server for declarative JWT verification
  • NestJS: Use the @nestjs/jwt package with Passport strategies for guard-based authentication
  • Go (Gin/Echo): Use middleware libraries like gin-jwt or build custom middleware with golang-jwt

Conclusion

JWT is the standard for microservice authentication because it's stateless and self-contained. Use an API Gateway for centralized authentication, RS256 with JWKS for distributed verification, and scope-based authorization for fine-grained access control. Use our JWT decoder to inspect tokens and verify they contain the right claims for your microservice architecture.

Need to decode a JWT token? Try our free JWT decoder tool — no sign-up required, runs entirely in your browser.