diff --git a/src/McpServer/autopilot-monitor-mcp/src/oauth.ts b/src/McpServer/autopilot-monitor-mcp/src/oauth.ts index c537f4dc..08d4ca93 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/oauth.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/oauth.ts @@ -55,12 +55,31 @@ const registeredClients = new Map { + const raw = process.env.OAuthStateSigningKey; + if (!raw) return crypto.randomBytes(32); + // Buffer.from(s, 'base64') silently drops invalid chars — the only useful + // signal is the resulting byte length. Anything below 32 bytes is either a + // malformed input or a too-weak key; either way we want to fail loud at + // boot rather than ship a degraded-security configuration. + const decoded = Buffer.from(raw, 'base64'); + if (decoded.length < 32) { + throw new Error( + `OAuthStateSigningKey must be a base64-encoded value yielding ≥32 bytes after decode (got ${decoded.length}). ` + + 'Generate one with PowerShell: ' + + '[Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) ' + + "or Node: node -e \"console.log(require('crypto').randomBytes(32).toString('base64'))\"", + ); + } + return decoded; +})(); const STATE_MAX_AGE_SECONDS = 600; interface ProxyStatePayload { @@ -74,7 +93,7 @@ export function signState(payload: Omit): string { const full: ProxyStatePayload = { ...payload, iat: Math.floor(Date.now() / 1000) }; const json = JSON.stringify(full); const body = Buffer.from(json, 'utf-8').toString('base64url'); - const sig = crypto.createHmac('sha256', STATE_HMAC_KEY).update(body).digest('base64url'); + const sig = crypto.createHmac('sha256', OAUTH_STATE_SIGNING_KEY).update(body).digest('base64url'); return `${body}.${sig}`; } @@ -83,7 +102,7 @@ export function verifyState(state: string, nowSeconds: number = Math.floor(Date. if (parts.length !== 2) return null; const [body, sig] = parts; - const expected = crypto.createHmac('sha256', STATE_HMAC_KEY).update(body).digest('base64url'); + const expected = crypto.createHmac('sha256', OAUTH_STATE_SIGNING_KEY).update(body).digest('base64url'); // Constant-time compare; lengths must match before timingSafeEqual. const sigBuf = Buffer.from(sig, 'utf-8'); const expBuf = Buffer.from(expected, 'utf-8');