JWT Across Microservices: How to Do It Without Repeating Yourself

by Arif Ikhsanudin, Backend Developer

The copy-paste security problem

You have eight services. Each one implements JWT validation — the same token extraction, the same signature verification against the same public key, the same expiry check, the same claim parsing. When your auth requirements change (new signing algorithm, new required claim, new validation rule), you update the logic in service one, test it, and then spend the next two weeks making the same change in services two through eight, hoping nobody misses a step.

This is not a hypothetical friction — it's a real pattern in organizations that adopted microservices without thinking through auth first. The consequences range from annoying (upgrade coordination across eight teams) to dangerous (one team forgets to update their validation logic, runs with a deprecated algorithm that's now vulnerable, and nobody notices for three months).

The two defensible architectures

Gateway validation with identity forwarding: validate the JWT once at the API gateway (Kong, NGINX, AWS API Gateway, or a custom gateway service). The gateway verifies signature, expiry, and required claims. On success, it strips the Authorization header and forwards validated identity claims as internal headers to downstream services.

# NGINX with lua-resty-jwt: validate at gateway, forward claims
location /api/ {
    access_by_lua_block {
        local jwt = require("resty.jwt")
        local token = ngx.var.http_authorization:match("Bearer%s+(.+)")
        local verified = jwt:verify(os.getenv("JWT_PUBLIC_KEY"), token)
        
        if not verified["verified"] then
            ngx.status = 401
            ngx.exit(401)
        end
        
        -- Forward verified claims as internal headers
        ngx.req.set_header("X-User-Id", verified.payload.sub)
        ngx.req.set_header("X-User-Roles", table.concat(verified.payload.roles, ","))
        ngx.req.clear_header("Authorization")
    }
    proxy_pass http://upstream;
}

Downstream services trust X-User-Id and X-User-Roles without re-validating the JWT. This is the single-validation pattern — one place to update when auth logic changes.

The security concern with this approach: if any downstream service is reachable without going through the gateway (direct cluster access, misconfigured network policy), it will accept forged X-User-Id headers. Mitigate this with Kubernetes NetworkPolicy that restricts all ingress to downstream services to come only from the gateway's IP range or pod label.

Per-service JWT validation with a shared library: each service validates the JWT, but validation logic lives in a single shared library. Services consume the library, and when validation logic changes, the library is updated and all services pick it up on their next deploy.

// Shared auth library: one place to update
@Library("auth-commons:2.3.1")
public class JwtValidator {
    private final JwksClient jwksClient; // fetches public keys from JWKS endpoint
    
    public UserIdentity validate(String token) {
        Jwt jwt = Jwts.parserBuilder()
            .setSigningKeyResolver(jwksClient.resolver())
            .requireIssuer("https://auth.internal")
            .requireAudience("internal-services")
            .build()
            .parseClaimsJws(token);
        
        return UserIdentity.from(jwt.getBody());
    }
}

This approach keeps JWT validation distributed (each service validates independently, no trust of internal headers) but centralizes the logic. The downside is that services must be redeployed to pick up library updates — you don't get a single configuration change that takes effect immediately.

JWKS: the right way to distribute public keys

Hardcoding the public key in each service's configuration means a key rotation requires touching every service. Use JWKS (JSON Web Key Set — RFC 7517): your auth server exposes a /jwks.json endpoint with the current public keys. Services fetch and cache this endpoint:

// https://auth.internal/.well-known/jwks.json
{
  "keys": [{
    "kty": "RSA",
    "kid": "2026-04-key-1",
    "use": "sig",
    "n": "...",
    "e": "AQAB"
  }]
}

The kid (key ID) in the JWT header tells the validator which key to use. During key rotation, the JWKS endpoint serves both old and new keys simultaneously. Old tokens (signed with the old key) continue to validate until they expire. New tokens use the new key. No service configuration changes needed.

Libraries like java-jwt (Auth0), nimbus-jose-jwt, and python-jose all support JWKS-based validation natively.

Claims to include and claims to avoid

JWTs in microservices often become kitchen sinks — every attribute the auth team thought might be useful gets stuffed in. This creates problems: tokens get large (JWT size matters in HTTP headers), claims go stale (a premium claim embedded in a 24-hour token remains true even if the user's subscription lapses), and different services start relying on different subsets of claims in undocumented ways.

Keep JWTs lean: subject (sub), issuer (iss), audience (aud), expiry (exp), and roles/permissions. For everything else — detailed user profile, current subscription state, feature entitlements — call the authoritative service or maintain a local cache updated via events.

A 1-hour JWT with minimal claims is a better tradeoff than a 24-hour JWT stuffed with mutable user attributes that will be stale by the end of the day.

The one thing to do today

Audit your current services: how many of them have independent JWT validation logic? If the answer is more than one and it's not from a shared library, that's your starting point. Extract the validation logic into a shared library with a test suite, version it, and start migrating services to it one at a time. The migration from N independent implementations to one library pays back immediately in reduced surface area for auth bugs — and significantly when you next need to update your token validation requirements.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Hiring a Senior Backend Developer in Singapore Takes 9 Weeks and S$120K — There Is a Better Way

Nine weeks ago you posted the job. Today you have two maybes, one lowball counteroffer, and a product launch that can't wait any longer.

Read more

Unit Tests That Are Hard to Write Are Telling You Something About Your Code

When a unit test requires elaborate setup, deep mocking, or access to private internals just to exercise a simple behavior, the test is not the problem. The design is. Testability friction is design feedback.

Read more

Why Barcelona Tech Startups Are Structuring Backend Work Around Contractors, Not Full-Time Hires

Some Barcelona startups have stopped treating every backend need as a headcount decision. The way they're structured explains why they keep shipping.

Read more

Red Flags in a Client Brief That You Should Not Ignore

Some client briefs are invitations to a good engagement. Others are invitations to a difficult one. The difference is usually visible in the brief itself, if you know what to look for.

Read more