FuntranslatorCreate Fun Language Translations
Free
Back to Blog
January 25, 202612 min read

How to Rotate JWT Secrets Without Downtime (kid + Multiple Keys)

Learn how to implement zero-downtime JWT secret rotation using the kid header claim and multiple key support. Includes Node.js code examples and migration strategies.

JWT secret rotation is a critical security practice, but doing it without service disruption requires careful planning. This guide shows you how to rotate JWT secrets with zero downtime using the kid (Key ID) header claim and multiple key support.

Why Rotate JWT Secrets?

Regular key rotation limits the impact of potential compromises:

  • Limit exposure window - If a key is compromised, only tokens signed during that period are affected
  • Compliance requirements - Many regulations mandate periodic key rotation
  • Security hygiene - Proactive rotation is better than reactive rotation after a breach

Without proper planning, rotation requires all users to re-authenticate. Let's avoid that.

The Problem with Naive Rotation

A simple rotation approach fails because:

  1. You deploy a new secret to your auth service
  2. Existing tokens signed with the old secret become invalid
  3. All users get 401 errors and must re-login
// NAIVE: This breaks existing tokens
// Before rotation
const token = jwt.sign(payload, oldSecret);

// After rotation - this throws Invalid signature!
const decoded = jwt.verify(token, newSecret);

The Solution: Multiple Keys with kid

The kid (Key ID) header claim identifies which key was used to sign a token. By supporting multiple keys simultaneously, you can:

  1. Issue new tokens with a new key (new kid)
  2. Verify tokens with either old or new key
  3. After transition period, stop accepting the old key

Implementation

Step 1: Key Store Structure

// keys.js
const crypto = require('crypto');

class KeyStore {
  constructor() {
    // Store multiple keys with metadata
    this.keys = new Map();
  }

  addKey(kid, secret, isActive = true) {
    this.keys.set(kid, {
      kid,
      secret,
      isActive,
      createdAt: Date.now()
    });
  }

  getActiveKey() {
    // Return the current signing key
    for (const [kid, key] of this.keys) {
      if (key.isActive) return key;
    }
    throw new Error('No active key');
  }

  getKey(kid) {
    return this.keys.get(kid);
  }

  getAllVerificationKeys() {
    // Return all keys for verification
    return Array.from(this.keys.values());
  }
}

module.exports = KeyStore;

Step 2: Token Signing with kid

// auth.js
const jwt = require('jsonwebtoken');

function signToken(payload, keyStore) {
  const activeKey = keyStore.getActiveKey();
  
  return jwt.sign(payload, activeKey.secret, {
    algorithm: 'HS256',
    keyid: activeKey.kid,  // Include kid in header
    expiresIn: '1h'
  });
}

// Example usage
const keyStore = new KeyStore();
keyStore.addKey('key-2026-01', process.env.JWT_SECRET_V1, true);

const token = signToken({sub: 'user-123', role: 'admin'}, keyStore);
// Header: {"alg": "HS256", "typ": "JWT", "kid": "key-2026-01"}

Step 3: Token Verification with Multiple Keys

// middleware.js
const jwt = require('jsonwebtoken');

function verifyToken(token, keyStore) {
  // Decode header without verifying
  const decoded = jwt.decode(token, {complete: true});
  
  if (!decoded || !decoded.header.kid) {
    throw new Error('Missing kid in token header');
  }

  const key = keyStore.getKey(decoded.header.kid);
  
  if (!key) {
    throw new Error('Unknown key ID');
  }

  // Verify with the specific key
  return jwt.verify(token, key.secret, {
    algorithms: ['HS256']
  });
}

// Express middleware
function authMiddleware(keyStore) {
  return (req, res, next) => {
    const token = req.headers.authorization?.split(' ')[1];
    
    if (!token) {
      return res.status(401).json({error: 'No token provided'});
    }

    try {
      req.user = verifyToken(token, keyStore);
      next();
    } catch (err) {
      res.status(401).json({error: 'Invalid token'});
    }
  };
}

Step 4: Rotation Process

// rotation.js
const crypto = require('crypto');

async function rotateKey(keyStore) {
  // 1. Generate new key
  const newKid = `key-${new Date().toISOString().slice(0, 7)}`;
  const newSecret = crypto.randomBytes(32).toString('hex');
  
  // 2. Add new key as active
  const oldActiveKey = keyStore.getActiveKey();
  keyStore.addKey(newKid, newSecret, true);
  
  // 3. Mark old key as inactive (but still valid for verification)
  keyStore.keys.get(oldActiveKey.kid).isActive = false;
  
  // 4. Store new secret in environment/KMS
  process.env.JWT_SECRET_NEW = newSecret;
  
  console.log(`Rotated to new key: ${newKid}`);
  console.log(`Old key still valid for verification: ${oldActiveKey.kid}`);
  
  return { newKid, newSecret };
}

// Schedule old key removal after transition period
async function removeOldKey(keyStore, kid, gracePeriodDays = 7) {
  setTimeout(() => {
    keyStore.keys.delete(kid);
    console.log(`Removed old key: ${kid}`);
  }, gracePeriodDays * 24 * 60 * 60 * 1000);
}

Rotation Timeline

Here's a typical rotation schedule:

DayAction
Day 0Generate new key, set as active, old key stays for verification
Day 1-7New tokens use new key, old tokens verified with old key
Day 7Remove old key from verification (tokens older than 7 days should be expired anyway)
Day 90Schedule next rotation

Token Expiration and Rotation

Your token expiration time should be shorter than your rotation grace period:

  • Token expiration: 1 hour to 24 hours
  • Rotation grace period: 7 days
  • Rotation frequency: Every 90 days

With 1-hour tokens and 7-day grace period, by day 7 all old tokens will have naturally expired.

RS256 Rotation (Easier!)

With RS256, rotation is even easier because you only need to rotate the private key. The public key for verification can be distributed via JWKS endpoint.

// JWKS endpoint for RS256
const express = require('express');
const app = express();

app.get('/.well-known/jwks.json', (req, res) => {
  const jwks = {
    keys: keyStore.getAllPublicKeys().map(key => ({
      kid: key.kid,
      kty: 'RSA',
      alg: 'RS256',
      use: 'sig',
      n: key.publicKeyN,
      e: 'AQAB'
    }))
  };
  res.json(jwks);
});

Best Practices

  1. Automate rotation - Don't rely on manual processes
  2. Monitor key usage - Track which keys are being used
  3. Have a rollback plan - Be able to revert if issues arise
  4. Test rotation - Practice in staging before production
  5. Document the process - Your team should know how to rotate keys

Tools

Related Articles

JWT Best Practices Checklist (Copy/Paste for Production)

A comprehensive checklist of JWT security best practices for production applications. Copy and paste to ensure your JWT implementation is secure.

Read Article

Where to Store JWT Secrets: Env Vars vs Vault vs KMS

Compare storage options for JWT secrets: environment variables, HashiCorp Vault, and cloud KMS. Learn when to use each and implementation best practices.

Read Article