BlogNever Hardcode JWT Secrets: What Goes Wrong and How to Fix It
·Updated July 22, 2026·5 min read·JWTSecrets Team

Never Hardcode JWT Secrets: What Goes Wrong and How to Fix It

Hardcoded JWT secrets leak through git history and public repos. Here is how exposure happens and how to store and rotate secrets the right way.

Hardcoded JWT Secrets Still Show Up Everywhere

Every week, security researchers find hardcoded JWT secrets in public GitHub repositories. Some of those repositories have tens of thousands of stars. The developers who shipped them are not careless — they often did not understand the chain of consequences from a "temporary" string in auth.js to permanent compromise.

This guide explains how hardcoded secrets leak, why git history makes deletion insufficient, how to load secrets from the environment safely, and what to do if you already committed one. Generate replacements with the JWT Secret Generator before you rotate.

How Hardcoded Secrets Get Exposed

The code starts innocuously:

// auth.js
const jwt = require('jsonwebtoken');
const SECRET = 'mysupersecretkey'; // Fine for testing, right?

app.post('/login', (req, res) => {
  const token = jwt.sign({ userId: user.id }, SECRET);
  res.json({ token });
});

It gets pushed to GitHub. Maybe the repo is private. Three months later, someone opens it for a demo. Or the organisation flips visibility by mistake. Or GitHub secret scanning notifies you — after bots that index secrets have already scraped the commit.

Once a secret is in git history, treat it as compromised permanently. Deleting the line in a later commit does not erase earlier blobs. Forks and clones retain the old value. git filter-branch or BFG can rewrite history, but anyone who cloned before the rewrite still has the secret.

The Git History Problem

# An attacker can find your old secret even after you delete the file
git log --all --full-history -- auth.js
git show <old-commit-hash>:auth.js

The fix is not "delete the file and push a new commit." The fix is: rotate the secret immediately, then clean history, then prevent recurrence with scanning and hooks.

The Correct Approach: Environment Variables

# .env (local development only — in .gitignore)
JWT_SECRET=<generated-at-jwtsecretgenerator.com>

# .env.example (safe to commit — no real values)
JWT_SECRET=your-256-bit-secret-here
// auth.js — correct approach
const jwt = require('jsonwebtoken');

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

const token = jwt.sign({ userId: user.id }, SECRET, {
  algorithm: 'HS256',
  expiresIn: '15m',
});

The if (!SECRET) throw check matters — it makes missing secrets a startup failure rather than a runtime bug that signs tokens with undefined or an empty string.

Never commit .env. Commit .env.example with placeholders only. Document generation via /tools/jwt-secret-generator in your README so new developers do not invent weak strings.

Platform-Specific Storage

Vercel:

Dashboard → Project → Settings → Environment Variables → Add JWT_SECRET

Railway:

Project → Variables → Add variable

Heroku:

heroku config:set JWT_SECRET=your-secret-here

AWS (Lambda or ECS):

Store in AWS Secrets Manager, fetch at startup:

const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

async function loadJwtSecret() {
  const client = new SecretsManagerClient({ region: 'us-east-1' });
  const { SecretString } = await client.send(
    new GetSecretValueCommand({ SecretId: 'prod/jwt-secret' }),
  );
  if (!SecretString) throw new Error('Empty secret');
  return SecretString;
}

Kubernetes:

apiVersion: v1
kind: Secret
metadata:
  name: jwt-secret
type: Opaque
stringData:
  JWT_SECRET: "your-secret-here"

Mount as an environment variable in the pod spec. Prefer external secret operators in larger clusters so values never sit in plain manifests in git.

For deeper storage patterns, see where to store JWT secrets and how to store JWT secrets securely.

What to Do If You Already Committed a Secret

1. Generate a new secret immediately at the JWT Secret Generator.

2. Deploy the new secret to production using a grace-period rotation.

3. Treat old tokens signed with the leaked secret as untrusted; shorten TTLs and invalidate refresh sessions if needed.

4. Clean git history with BFG Repo Cleaner (bfg --replace-text secrets.txt) or equivalent.

5. Force-push only after coordinating with the team — and assume prior clones retain the leak.

6. Enable GitHub secret scanning on all repositories.

7. Add a pre-commit hook with gitleaks or detect-secrets.

Pre-commit Hook Setup

# Install gitleaks
brew install gitleaks  # macOS
# or: go install github.com/gitleaks/gitleaks/v8@latest

# Add to .git/hooks/pre-commit
#!/bin/bash
gitleaks protect --staged -v
if [ $? -ne 0 ]; then
  echo "Secret detected. Commit blocked."
  exit 1
fi

CI should run the same scanner on every pull request. Hooks alone are not enough — they are skipped with --no-verify.

Production Checklist

  • [ ] No JWT secrets in source, Docker images, or ticket screenshots
  • [ ] Startup fails closed when JWT_SECRET is missing
  • [ ] Dev / staging / production secrets differ
  • [ ] Secret scanning enabled on the host platform
  • [ ] Rotation runbook tested
  • [ ] Algorithm allowlists set on verify

Frequently Asked Questions

Is a private GitHub repository safe enough for hardcoded secrets?

No. Private repos still leak through misconfiguration, contractor access, laptop theft, and tooling that indexes clones. Secrets belong in environment configuration or a vault, not in blobs.

Can I keep a hardcoded secret for local development only?

Prefer a local .env that is gitignored. If you must ship a demo, use an obviously fake placeholder and refuse to start when the placeholder is detected in non-dev environments.

What is the difference between encoding a secret in Base64 and encrypting it?

Base64 is encoding, not protection. Anyone who reads the string recovers the bytes. Encryption and vault storage protect confidentiality; Base64 only changes representation. See Base64 in JWTs.

How often should I rotate after a hardcoded secret leak?

Immediately. Do not wait for a quarterly schedule. Use dual-secret acceptance until access tokens expire, then remove the leaked key permanently.

Written by

JWTSecrets Team

Editorial Team

The JWTSecrets editorial team writes practical guides on JWT authentication, cryptographic key management, and browser-based security tooling. Our content is reviewed against IETF RFCs and current library documentation.