YAML Config for JWT Auth in Node.js —Secure Defaults Template
The first time a security audit flagged my project for hardcoded JWT secrets, I was embarrassed but not surprised. The secret was sitting in config.js right next to environment names and log levels. It had been there for months.
Configuration management for JWT authentication involves more than just moving secrets out of code. You need to handle:
- Secret storage and rotation
- Algorithm selection (symmetric vs asymmetric)
- Token expiration policies per environment
- Key ID management for multi-key support
- Audience and issuer validation rules
This guide provides a production-ready YAML configuration template for JWT auth in Node.js, explains each setting, and shows how to use environment variable interpolation to keep secrets out of version control. The YAML Formatter is useful for validating and formatting your configuration files as you work through the template.
Why YAML for JWT Config?
JSON is the default choice for Node.js configuration, but YAML offers advantages for auth configuration:
- Comments: Document why each setting exists and what its security implications are
- Multiline strings: Cleaner for PEM-encoded public keys and certificate blocks
- Anchors and aliases: Reference the same value in multiple places without duplication
- Nested structure: Natural grouping of algorithm configs, key sets, and validation rules
The tradeoff is that YAML whitespace sensitivity can introduce subtle parsing errors. Run every config file through a linter or formatter before deployment.
The Template
# config/auth.yaml —JWT Authentication Configuration
# All secrets reference environment variables.
# Never commit resolved secrets to version control.
environment: ${NODE_ENV:-development}
jwt:
# Access token configuration
access_token:
algorithm: RS256
private_key: ${JWT_PRIVATE_KEY}
public_key: ${JWT_PUBLIC_KEY}
expiration: 15m
issuer: https://api.devutils.com
audience: devutils-api
# Refresh token configuration
refresh_token:
algorithm: HS256
secret: ${JWT_REFRESH_SECRET}
expiration: 7d
issuer: https://api.devutils.com
audience: devutils-api
# Key management
keys:
current_kid: key-2026-01
rotation:
enabled: true
grace_period: 24h
notify_before: 7d
store:
- kid: key-2026-01
algorithm: RS256
source: environment
- kid: key-2026-02
algorithm: RS256
source: environment
# Validation rules
validation:
clock_tolerance: 30
max_iat_age: 5m
required_claims:
- iss
- sub
- exp
- iat
reject_before: ${JWT_REJECT_BEFORE:-0}
# Rate limiting by token
rate_limit:
enabled: true
max_tokens_per_user: 5
cleanup_interval: 1h
Template Walkthrough
Environment Selector
environment: ${NODE_ENV:-development}
The ${VARIABLE:-default} syntax provides environment variable interpolation with a fallback default. This keeps the same config file usable across development, staging, and production without modification.
Asymmetric Access Tokens
access_token:
algorithm: RS256
private_key: ${JWT_PRIVATE_KEY}
public_key: ${JWT_PUBLIC_KEY}
expiration: 15m
Access tokens are short-lived credentials presented with every API request. Using RS256 (RSA-SHA256) means:
- Your auth server signs tokens with the private key
- Any service can verify tokens with the public key without sharing secrets
- If a service is compromised, the public key can be rotated without updating every verifier
The 15-minute expiration is the current security best practice. Longer expirations increase the window for token theft; shorter expirations increase auth server load with refresh token exchanges.
Symmetric Refresh Tokens
refresh_token:
algorithm: HS256
secret: ${JWT_REFRESH_SECRET}
expiration: 7d
Refresh tokens use HS256 (HMAC-SHA256) —a shared secret between the auth server and the token database. Refresh tokens are long-lived and should only be verified by the auth service itself, so symmetric signing is appropriate here.
The 7-day expiration balances user convenience with security. If a refresh token is stolen, the window of exposure is limited to one week.
Key Rotation
rotation:
enabled: true
grace_period: 24h
notify_before: 7d
Key rotation is the practice of periodically generating new signing keys. The grace_period specifies how long old keys remain valid after rotation —tokens signed with the old key continue to work for 24 hours, allowing verifiers to pick up the new key. notify_before triggers a warning 7 days before the scheduled rotation.
The key store supports multiple keys simultaneously:
store:
- kid: key-2026-01
algorithm: RS256
- kid: key-2026-02
algorithm: RS256
When verifying a token, look up the key by the kid header claim. When signing, use the key identified by current_kid.
Loading the Config in Node.js
// config/loader.js
const fs = require('fs');
const path = require('path');
const yaml = require('yaml');
function loadConfig() {
const raw = fs.readFileSync(
path.join(__dirname, 'auth.yaml'),
'utf-8'
);
// Interpolate environment variables
const interpolated = raw.replace(
/\$\{([^}]+)\}/g,
(match, expression) => {
const [variable, defaultValue] = expression.split(':-');
return process.env[variable] || defaultValue || match;
}
);
return yaml.parse(interpolated);
}
const config = loadConfig();
module.exports = config;
Using the Config for JWT Operations
Signing
const jwt = require('jsonwebtoken');
const config = require('./config/loader');
function signAccessToken(userId, claims = {}) {
const { access_token } = config.jwt;
const options = {
algorithm: access_token.algorithm,
expiresIn: access_token.expiration,
issuer: access_token.issuer,
audience: access_token.audience,
subject: userId,
header: {
kid: config.jwt.keys.current_kid,
},
};
return jwt.sign(
{ ...claims, sub: userId },
access_token.private_key,
options
);
}
Verification
function verifyAccessToken(token) {
const { access_token, keys } = config.jwt;
// Decode without verification to get the kid
const decoded = jwt.decode(token, { complete: true });
const keyEntry = keys.store.find(k => k.kid === decoded.header.kid);
if (!keyEntry) {
throw new Error('Unknown key ID: ' + decoded.header.kid);
}
const publicKey = process.env['JWT_PUBLIC_KEY_' + keyEntry.kid.toUpperCase()];
return jwt.verify(token, publicKey, {
algorithms: [access_token.algorithm],
issuer: access_token.issuer,
audience: access_token.audience,
clockTolerance: config.jwt.validation.clock_tolerance,
});
}
The jwt.decode call reads the header without verification, then jwt.verify uses the correct key based on kid. This allows multiple keys to coexist during rotation.
Environment File Example
# .env.production —Do not commit to version control
# Source during deployment or container initialization
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA..."
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0B..."
JWT_REFRESH_SECRET="a7f3c9e1b2d84f5a6e7c8d9e0f1a2b3c"
JWT_REJECT_BEFORE="1704067200"
PEM-formatted keys in environment variables require careful handling. The \n literal newlines in the variable value must be processed when loading:
function normalizePem(pem) {
return pem ? pem.replace(/\\n/g, '\n') : null;
}
Security Checklist
- Never commit
.envfiles —Add to.gitignoreimmediately - Use asymmetric algorithms for access tokens —RS256 or ES256, never HS256 for distributed verification
- Validate all claims —At minimum
exp,iss,aud, andsub - Set short access token expirations —15 minutes or less
- Implement key rotation —At minimum, rotate keys when a team member leaves
- Use
clockTolerancesparingly —30 seconds maximum. High tolerance weakens expiration guarantees - Log verification failures —Invalid signatures, expired tokens, and unknown key IDs all indicate potential attacks
FAQ
Why RS256 over HS256 for access tokens?
RS256 uses asymmetric keys. Any service with the public key can verify tokens, but only the auth server can sign them. HS256 requires sharing the same secret with every verifier —if any verifier is compromised, the entire auth system is compromised.
How often should I rotate JWT signing keys?
Every 3— months for standard deployments, or immediately after any security incident. Automated rotation with a grace period ensures smooth transitions.
Can I store JWK-formatted keys in YAML?
Yes. The JWK (JSON Web Key) format represents keys as structured JSON, which embeds cleanly into YAML:
keys:
- kid: key-rs256-01
kty: RSA
alg: RS256
n: ${JWT_KEY_N}
e: ${JWT_KEY_E}
But PEM strings are more widely supported by JWT libraries across languages and ecosystems.
Should I use different keys for different environments?
Absolutely. Development keys should never be the same as production keys. Use separate environment variables per environment and validate that the correct set is loaded during startup.
What happens during key rotation?
- Generate a new key pair with a new
kid - Add it to the key store alongside existing keys
- Update
current_kidto the new key - Existing tokens signed with the old key remain valid for the grace period
- After the grace period, remove the old key from the store
- Tokens signed with the removed key are rejected
Final Thoughts
JWT configuration is one of those things that looks trivial until a misconfiguration creates a security hole. A hardcoded secret, an expired key no one remembered to rotate, an alg: none acceptance that a library's default allowed —these are the bugs that show up in penetration test reports, not unit test failures.
The YAML template in this guide gives you a starting point with secure defaults. Adapt the expiration times to your threat model, add keys as your service scales, and automate the rotation process before you need it rather than after.
For validating your configuration, the YAML Formatter catches indentation and structure issues before they cause runtime errors. Use the JWT Decoder to inspect tokens signed with your config and verify they contain the expected claims. The Base64 Encoder & Decoder is useful for inspecting raw key material when debugging key format issues.
For more on JWT best practices, see JWT Decode vs Verify —What Each Step Actually Does and Base64 Is Not Encryption. To understand common YAML configuration pitfalls, check Best YAML Formatter Tools and Validation Tips.