From e4bc705504e905505cb0ad3b685c86c6e0bbff95 Mon Sep 17 00:00:00 2001 From: Matthew Berman <748450+mberman84@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:44:59 -0700 Subject: [PATCH] Add hidden GitHub auth canary entry --- worker/src/auth-votes.js | 54 +++++++++++++++++++++++++++++++++- worker/test/auth-votes.test.js | 12 +++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/worker/src/auth-votes.js b/worker/src/auth-votes.js index 68717cb..c94da10 100644 --- a/worker/src/auth-votes.js +++ b/worker/src/auth-votes.js @@ -128,7 +128,11 @@ async function startOAuth(requestUrl, env) { return unavailable("Login is not configured."); } - const clientNonce = requestUrl.searchParams.get("client_nonce") || ""; + const requestedNonce = requestUrl.searchParams.get("client_nonce"); + if (requestedNonce === null) { + return oauthStartBridge(requestUrl, env); + } + const clientNonce = requestedNonce; if (!OAUTH_NONCE_PATTERN.test(clientNonce)) { return jsonResponse( { error: "Invalid OAuth nonce", code: "invalid_oauth_nonce" }, @@ -161,6 +165,54 @@ async function startOAuth(requestUrl, env) { return jsonResponse({ authorizationUrl: authorizationUrl.toString() }); } +function oauthStartBridge(requestUrl, env) { + const startUrl = new URL( + `${canonicalOrigin(env)}${normalizeBasePath( + env.PUBLIC_SITE_PATH || "/loop-library", + )}/auth/github`, + ); + startUrl.searchParams.set( + "return_to", + safeReturnTo(requestUrl.searchParams.get("return_to"), env), + ); + const bridge = scriptJson({ startUrl: startUrl.toString() }); + const body = ` +
Starting GitHub sign-in…
+`; + return new Response(body, { + status: 200, + headers: { + "Cache-Control": "no-store", + "Content-Security-Policy": `default-src 'none'; script-src 'unsafe-inline'; connect-src 'self' ${canonicalOrigin(env)}; base-uri 'none'; frame-ancestors 'none'`, + "Content-Type": "text/html; charset=utf-8", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + }, + }); +} + async function finishOAuth(request, requestUrl, env, fetcher) { const state = requestUrl.searchParams.get("state") || ""; const oauth = await readSignedValue(state, env.SESSION_SECRET); diff --git a/worker/test/auth-votes.test.js b/worker/test/auth-votes.test.js index 9281446..30be427 100644 --- a/worker/test/auth-votes.test.js +++ b/worker/test/auth-votes.test.js @@ -291,7 +291,17 @@ test("GitHub OAuth state is verified and X routes are absent", async () => { new Request(`${BASE}/auth/github?return_to=%2Floop-library%2F`), env, ); - assert.equal(missingNonce.status, 400); + assert.equal(missingNonce.status, 200); + const startBridge = await missingNonce.text(); + assert.match(startBridge, /sessionStorage\.setItem\("ll_oauth_nonce"/); + assert.match(startBridge, /client_nonce/); + assert.match(startBridge, /GitHub sign-in/); + + const malformedNonce = await handleAuthVoteRoute( + new Request(`${BASE}/auth/github?client_nonce=too-short`), + env, + ); + assert.equal(malformedNonce.status, 400); assert.equal( await handleAuthVoteRoute(new Request(`${BASE}/auth/x`), env),