session-secret-rotation.mjs
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
10 days ago
| 1 | /** |
| 2 | * SEC-KN-P6-ROTATE — dual-secret JWT verify helper for zero-downtime |
| 3 | * SESSION_SECRET rotation (docs/SEC-KN-P6-ROTATE-FREEZE.md §6.2, P6-C1). |
| 4 | * |
| 5 | * During a rotation window the gateway, bridge, and EC2 MCP host each carry: |
| 6 | * - SESSION_SECRET (primary — signs new JWTs, tried first on verify) |
| 7 | * - SESSION_SECRET_PREVIOUS (verify-only — accepted if primary verify fails) |
| 8 | * |
| 9 | * Fail-closed rules (frozen): |
| 10 | * - No/empty primary → refuse verify (never a silent "previous-only" sign-in). |
| 11 | * - previous === primary → previous is ignored (no second verify). |
| 12 | * - Signing always uses the primary secret only; this module never signs. |
| 13 | * - Errors are swallowed to null; secret material never appears in messages. |
| 14 | * |
| 15 | * Non-JWT HMAC/encrypt paths (bridge GitHub-token encrypt, image-proxy HMAC, |
| 16 | * internal request HMAC) are intentionally OUT of this helper — they stay |
| 17 | * primary-only (freeze §6.2 / §6.4). |
| 18 | */ |
| 19 | |
| 20 | import jwt from 'jsonwebtoken'; |
| 21 | |
| 22 | /** |
| 23 | * Verify an HS256 access JWT against the primary secret, falling back to the |
| 24 | * previous secret during a rotation window. |
| 25 | * |
| 26 | * @param {unknown} token - Bearer token string. |
| 27 | * @param {unknown} primary - Current SESSION_SECRET (signing + first verify). |
| 28 | * @param {unknown} previous - SESSION_SECRET_PREVIOUS (verify-only), or null/undefined. |
| 29 | * @returns {object|null} Verified payload, or null when verification fails or |
| 30 | * the primary secret is missing (fail closed). |
| 31 | */ |
| 32 | export function verifyJwtWithSecretRotation(token, primary, previous) { |
| 33 | if (typeof token !== 'string' || token === '') return null; |
| 34 | if (typeof primary !== 'string' || primary === '') return null; |
| 35 | try { |
| 36 | return jwt.verify(token, primary); |
| 37 | } catch (_) { |
| 38 | // fall through to the rotation window |
| 39 | } |
| 40 | if (typeof previous !== 'string' || previous === '' || previous === primary) return null; |
| 41 | try { |
| 42 | return jwt.verify(token, previous); |
| 43 | } catch (_) { |
| 44 | return null; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Resolve the verify-only previous secret from the environment. |
| 50 | * Kept as a function so call sites share one resolution rule. |
| 51 | * |
| 52 | * @param {NodeJS.ProcessEnv} [env] |
| 53 | * @returns {string|null} |
| 54 | */ |
| 55 | export function resolveSessionSecretPrevious(env = process.env) { |
| 56 | const v = env.SESSION_SECRET_PREVIOUS; |
| 57 | return typeof v === 'string' && v !== '' ? v : null; |
| 58 | } |
File History
1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
10 days ago