From 4a1cc1a9a2269f4dea08dd08f7ba55237d3c3363 Mon Sep 17 00:00:00 2001 From: viktormarinho Date: Thu, 20 Aug 2026 11:55:20 -0300 Subject: [PATCH] feat(github): seal the OAuth state and authorization code The authorization code this server hands back is the GitHub access and refresh token in plain base64url JSON, and the state carrying the redirect URI and PKCE challenge is unsigned. Anyone who observes a code, through a referrer, a proxy log or browser history, holds a live repo read:org read:user token without ever calling /token. @decocms/runtime 3.0.0 seals both with AES-GCM when given a stateSecret, so read OAUTH_STATE_SECRET and pass it through. getStateSecret() throws when the variable is missing rather than letting the runtime fall back to plaintext. A silent downgrade here looks identical to working and is exactly the failure this is meant to prevent. Set the secret before merging: cd github && bunx wrangler secret put OAUTH_STATE_SECRET Deploying this without it takes the worker down, which is the intended direction to fail. In-flight authorizations do not survive the rollout: a state issued before it is plaintext, and the sealed build refuses plaintext by design. Users retry and it works. --- github/server/main.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/github/server/main.ts b/github/server/main.ts index 8ac27ad0..0a642c39 100644 --- a/github/server/main.ts +++ b/github/server/main.ts @@ -41,6 +41,27 @@ type Runtime = ReturnType< const REQUESTED_SCOPES = "repo read:org read:user"; +/** + * Key material that seals the OAuth `state` and the authorization `code`. + * + * Unset, the runtime falls back to plain base64url JSON and the code carries + * the GitHub access token in the clear, so this throws rather than silently + * downgrading. Set it with `bunx wrangler secret put OAUTH_STATE_SECRET` + * before deploying a build that reads it. + * + * Read per-call for the same reason as the credentials below. + */ +function getStateSecret(): string { + const stateSecret = process.env.OAUTH_STATE_SECRET || ""; + if (!stateSecret) { + throw new Error( + "OAuth state secret not configured. " + + "Set the OAUTH_STATE_SECRET environment variable.", + ); + } + return stateSecret; +} + /** * Lazily read OAuth credentials — on Workers, process.env isn't populated * at module-init time, so we must resolve per-call. @@ -69,6 +90,7 @@ async function getRuntime(): Promise { mode: "PKCE", authorizationServer: "https://github.com", allowedRedirectHosts: [...ALLOWED_REDIRECT_HOST_SUFFIXES], + stateSecret: getStateSecret(), authorizationUrl: (callbackUrl) => { const clientId = process.env.GITHUB_CLIENT_ID || "";