security2025-05-20·8·56/348

jsonwebtoken VerifyError: invalid signature

How to diagnose and fix JWT verification errors caused by invalid signatures, encoding issues, and secret key mismatches.

jsonwebtoken VerifyError: invalid signature

Introduction

JSON Web Tokens (JWT) are the backbone of most modern authentication systems. When token verification fails with VerifyError: invalid signature, it can lock users out of your application entirely. This post covers the most common causes and their fixes.

Environment

node --version
# v20.11.0

npm list jsonwebtoken
# my-app@1.0.0
# +-- jsonwebtoken@9.0.2

Problem

After deploying to a new server, all existing tokens started failing verification:

const jwt = require('jsonwebtoken');

try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  console.log(decoded);
} catch (err) {
  console.error(err);
  // JsonWebTokenError: invalid signature
}

The error occurred consistently for all tokens, even freshly generated ones.

Analysis

The invalid signature error means the token's signature does not match the expected signature computed from the header and payload using the provided secret. Common causes include:

  1. Secret key mismatch - Different secret used for signing vs verification
  2. Encoding issues - The secret contains special characters that are not handled consistently
  3. Algorithm mismatch - Token signed with one algorithm, verified with another
  4. Secret truncation - Environment variable has invisible characters or newlines

Here is how JWT signing works:

HMAC-SHA256(base64(header) + "." + base64(payload), secret) = signature

If any component changes, the signature will not match.

Solution

Solution 1: Verify secret consistency

// Check that your secret is what you expect
console.log('Secret length:', process.env.JWT_SECRET.length);
console.log('Secret hex:', Buffer.from(process.env.JWT_SECRET).toString('hex'));

// BAD: Secret loaded with trailing newline
// echo "my-secret-key" > .env  (adds newline!)
// process.env.JWT_SECRET = "my-secret-key\n"

// GOOD: Trim whitespace
const secret = process.env.JWT_SECRET.trim();

Solution 2: Specify algorithm explicitly

// Sign with explicit algorithm
const token = jwt.sign(
  { userId: 123 },
  process.env.JWT_SECRET,
  { algorithm: 'HS256', expiresIn: '1h' }
);

// Verify with explicit algorithm
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256']
});

Solution 3: Handle encoding properly

// If your secret contains unicode characters
const secret = Buffer.from(process.env.JWT_SECRET, 'utf-8').toString('utf-8');

// Use the same encoding for both signing and verification
const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });

Solution 4: Debug token contents

function debugToken(token) {
  const parts = token.split('.');
  
  console.log('Header:', JSON.parse(Buffer.from(parts[0], 'base64url')));
  console.log('Payload:', JSON.parse(Buffer.from(parts[1], 'base64url')));
  console.log('Signature:', parts[2]);
  
  // Verify manually
  const crypto = require('crypto');
  const hmac = crypto.createHmac('sha256', process.env.JWT_SECRET);
  hmac.update(`${parts[0]}.${parts[1]}`);
  const expectedSig = hmac.digest('base64url');
  
  console.log('Expected signature:', expectedSig);
  console.log('Match:', expectedSig === parts[2]);
}

debugToken(eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyM30.signature);

Solution 5: Complete auth middleware

const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
  throw new Error('JWT_SECRET environment variable is required');
}

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid authorization header' });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET, {
      algorithms: ['HS256']
    });
    
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

module.exports = authMiddleware;

Lessons Learned

  1. Never commit secrets to version control - Use environment variables or a secrets manager
  2. Always specify algorithms explicitly - Prevents algorithm confusion attacks
  3. Validate the secret length - HMAC-SHA256 needs at least 256 bits (32 bytes)
  4. Use .env files carefully - Watch for trailing newlines and whitespace
  5. Rotate secrets with a grace period - Accept both old and new secrets during rotation

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.