How to Fix JWT RS256 Signature Verification Failing With Public Key Format Error
Quick answer
Verifying a JWT signed with RS256 fails, and the error points at something wrong with the public key itself rather than the token's actual signature being...
Verifying a JWT signed with RS256 fails, and the error points at something wrong with the public key itself rather than the token's actual signature being invalid. RS256 verification is unforgiving about exact key formatting β a key that looks visually correct when printed can still fail to parse correctly due to a subtle formatting issue most libraries won't clearly explain.
The Problem
Verification fails with an error about the key rather than a signature mismatch:
Error: error:0909006C:PEM routines:get_name:no start line
at Object.createPrivateKey (node:internal/crypto/keys:...)
Or, depending on the library and language, a more generic but equally unhelpful message:
JWTError: Invalid key format
Sometimes verification runs without a parsing error but still fails the actual signature check:
jwt.exceptions.InvalidSignatureError: Signature verification failed
Why It Happens
RS256 uses asymmetric keys β a private key signs the token, and a matching public key verifies it β and this error covers a few distinct categories of problem, all surfacing similarly:
- Malformed PEM formatting β the public key is missing its
-----BEGIN PUBLIC KEY-----/-----END PUBLIC KEY-----markers, has incorrect line breaks (a common issue when a PEM key is passed through an environment variable and loses its newlines), or has extra whitespace corrupting the base64-encoded content. - Using the wrong key format for what the library expects β some libraries expect a raw PEM string, others expect a JWK (JSON Web Key) object, and passing one format where the other is expected fails, often with a confusing error rather than a clear format mismatch message.
- A genuine key mismatch β the public key you're using to verify doesn't actually correspond to the private key that signed the token, which happens after a key rotation where the verifying service wasn't updated with the new public key.
- Confusing the private and public key β accidentally configuring the verification step with the private key instead of the public key, or vice versa for the signing step.
The Fix
First, check the actual PEM string your code is using for obvious formatting corruption, especially if it came from an environment variable:
console.log(JSON.stringify(process.env.JWT_PUBLIC_KEY));
"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\\n-----END PUBLIC KEY-----"
If the newlines were flattened into literal \n characters that need actual conversion (a common issue with environment variable storage), explicitly convert them back to real newlines in code before using the key:
const publicKey = process.env.JWT_PUBLIC_KEY.replace(/\\n/g, '\n');
Verify the resulting key is well-formed PEM by attempting to parse it independently of your JWT library, which isolates whether the problem is the key itself or something in your verification call:
openssl pkey -pubin -in public_key.pem -text -noout
If this command fails, the PEM file itself is malformed β fix the formatting before worrying about anything JWT-specific.
If your library expects a JWK instead of PEM (common with libraries that fetch keys from a JWKS endpoint), convert accordingly rather than passing a raw PEM string where a JWK object is expected:
const jwksClient = require('jwks-rsa');
const client = jwksClient({jwksUri: 'https://auth.example.com/.well-known/jwks.json'});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.getPublicKey(); // library handles JWK-to-PEM conversion internally
callback(null, signingKey);
});
}
Confirm you're actually using the public key for verification, not the private key β a mix-up that's easy to make when both are stored in similarly named environment variables or configuration entries:
jwt.verify(token, publicKey, {algorithms: ['RS256']}); // must be the PUBLIC key here
If a key rotation recently happened at the issuing authorization server, make sure your service is fetching the current public key rather than a cached, now-outdated one β services using a JWKS endpoint should refresh their cached keys periodically rather than fetching once at startup and never again:
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
cacheMaxAge: 600000, // refresh cached keys every 10 minutes
});
Still Not Working?
If the key parses correctly and formatting looks fine but verification still fails, confirm the public key genuinely corresponds to the private key that actually signed this specific token β decode the token's header to check its kid (key ID) and cross-reference it against the specific key you're using, since a JWKS endpoint often serves multiple valid keys simultaneously during a rotation window:
echo "<token-header-base64>" | base64 -d
{"alg":"RS256","typ":"JWT","kid":"key-2026-08"}
Confirm the key you're using for verification matches this exact kid rather than assuming there's only one active key β a mismatch here, especially right after a key rotation, is a common source of intermittent verification failures that look identical to a formatting problem but are actually a straightforward key-selection bug.