Cybersecurity / Networking

JWT "Invalid Signature" Error When Validating Token Across Microservices

A JWT that validates fine within the service that issued it, but fails with "invalid signature" when validated by a different microservice, means the two services don't agree on the signing key or algorithm — this is a configuration synchronization problem across services, not a bug in the JWT library itself.

The Problem

A token issued by an authentication service is passed to a different downstream microservice, which rejects it:

JsonWebTokenError: invalid signature

jwt.exceptions.InvalidSignatureError: Signature verification failed

The exact same token validates successfully when checked against the issuing service directly, confirming the token itself is well-formed and the problem is specifically in how the second service attempts to verify it.

Mismatched signing keys across services Auth Service signs with Secret A Order Service verifies with Secret B

Why It Happens

A JWT's signature can only be verified successfully by something that has the exact same secret (for symmetric algorithms like HS256) or the correct corresponding public key (for asymmetric algorithms like RS256) that was used to originally sign it. Common causes of mismatch across services:

  • Different services have different values configured for the shared signing secret — often from separately managed environment variables that drifted out of sync, or a secret that was rotated in one service's configuration but not propagated to others
  • Using a symmetric algorithm (HS256) with a shared secret across many services, which requires every single service to have the exact same secret value — a pattern that becomes increasingly fragile and harder to keep synchronized as the number of services grows
  • A service expecting a different signing algorithm than what was actually used, particularly if migrating from HS256 to RS256 (or vice versa) wasn't rolled out consistently across every service simultaneously
  • For RS256 (asymmetric), a service using an outdated or incorrect public key — since the public key must correspond exactly to the private key that performed the signing, any mismatch here, even after a legitimate key rotation on the signing side, produces this exact error

The Fix

First, confirm both services are using the exact same secret or key by comparing configuration directly (being careful not to log or expose the actual secret value insecurely while doing so):

# Compare the actual configured values (redacted for illustration)
echo $JWT_SECRET | md5sum  # compare hashes across services rather than raw values in logs

Comparing a hash of the secret rather than the secret itself lets you confirm equality across services without actually exposing or logging the sensitive value directly.

For architectures with many microservices, strongly consider migrating from a shared symmetric secret (HS256) to an asymmetric algorithm (RS256), where only the issuing/authentication service holds the private signing key, and every other service only needs the public key to verify — this is both more secure (compromising a verifying service's config doesn't expose the ability to forge new tokens) and easier to manage at scale, since the public key can be freely distributed or fetched from a shared, non-sensitive endpoint:

# Auth service signs with private key
jwt.sign(payload, privateKey, { algorithm: 'RS256' });

// Every other service verifies with the public key only
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

If using RS256, confirm every verifying service is fetching the current public key correctly, ideally from a shared JWKS (JSON Web Key Set) endpoint rather than a manually copied and potentially stale key file:

curl https://auth-service.internal/.well-known/jwks.json

Using a JWKS endpoint that services fetch dynamically (with appropriate caching) rather than a manually distributed static key file significantly reduces the risk of key drift, since every service always fetches the currently correct public key directly from the authoritative source rather than relying on manual synchronization across deployments.

If you recently rotated the signing key or secret, confirm the rollout was coordinated correctly across every consuming service — ideally supporting a brief overlap period where both the old and new key are accepted during verification, preventing a window where tokens signed just before rotation fail validation immediately after:

const validKeys = [currentPublicKey, previousPublicKey]; // support both during rotation window
for (const key of validKeys) {
    try {
        return jwt.verify(token, key, { algorithms: ['RS256'] });
    } catch (e) { continue; }
}
throw new Error('Invalid signature with all known keys');

Still Not Working?

If secrets/keys are confirmed to match exactly and the algorithm is consistent, check whether the token itself was modified or re-encoded somewhere in transit between services — for example, a gateway or proxy that decodes and re-encodes the token for logging or transformation purposes could inadvertently alter it in a way that breaks the signature, even while leaving the token superficially readable and seemingly intact. Testing with the exact, unmodified token string captured directly from the issuing service's response, rather than one that's passed through any intermediate processing, isolates whether transit-related modification is the actual cause.