fix(auth): wire dual-key JWT verification into TokenService for WebSocket auth

Extract shared `verifyWithRotation` helper and `makeSecretOrKeyProvider` into
`jwt-rotation.ts` so both REST (passport-jwt strategy) and WebSocket
(TokenService.verifyAccessToken) paths honour JWT_SECRET_PREVIOUS during
secret rotation. Add env-validation for optional previous secrets and
document the rotation policy for WebSocket sessions.

Resolves GOO-237

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-24 14:44:23 +07:00
parent 455c959f44
commit 3705193f97
8 changed files with 345 additions and 252 deletions

View File

@@ -45,6 +45,17 @@ const REQUIRED_WHEN_USED: ReadonlyMap<string, string> = new Map([
* Known placeholder values that must never be used as real secrets.
* Comparison is case-insensitive to catch common variants.
*/
/**
* Previous-version secrets used during key rotation. Validated if set but never
* required. Note: JWT_REFRESH_SECRET_PREVIOUS currently has no runtime consumer
* because refresh tokens are opaque random bytes, not JWTs — the variable is
* accepted here for forward-compatibility should the refresh mechanism change.
*/
const OPTIONAL_PREVIOUS_SECRETS: readonly string[] = [
'JWT_SECRET_PREVIOUS',
'JWT_REFRESH_SECRET_PREVIOUS',
];
const FORBIDDEN_SECRET_VALUES: readonly string[] = [
'change_me',
'changeme',
@@ -127,6 +138,25 @@ export function validateEnv(): void {
);
}
// Validate optional previous secrets if they are set (rotation window).
const prevSecretErrors: string[] = [];
for (const key of OPTIONAL_PREVIOUS_SECRETS) {
const value = process.env[key];
if (value) {
const error = validateJwtSecret(key, value);
if (error) {
prevSecretErrors.push(error);
}
}
}
if (prevSecretErrors.length > 0) {
throw new Error(
`Insecure previous-secret configuration:\n ${prevSecretErrors.join('\n ')}\n` +
'Previous secrets must meet the same strength requirements as primary secrets.',
);
}
if (!isProduction) {
return;
}