From 3994405b378c2d62b4129a61ce7c92a40bef4365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nejc=20Drobni=C4=8D?= Date: Mon, 18 May 2026 12:42:31 +0000 Subject: [PATCH] fix: send required GitHub API headers --- apps/gateway/src/auth.ts | 15 +++++++- apps/gateway/test/index.spec.ts | 67 ++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/apps/gateway/src/auth.ts b/apps/gateway/src/auth.ts index 9673b74..53842fe 100644 --- a/apps/gateway/src/auth.ts +++ b/apps/gateway/src/auth.ts @@ -6,6 +6,11 @@ const GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize"; const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"; const GITHUB_USER_URL = "https://api.github.com/user"; const GITHUB_EMAILS_URL = "https://api.github.com/user/emails"; +const GITHUB_API_HEADERS = { + Accept: "application/vnd.github+json", + "User-Agent": "constructor-gateway", + "X-GitHub-Api-Version": "2022-11-28", +}; const PKCE_TTL_SECONDS = 600; // 10 minutes const JWT_TTL_SECONDS = 24 * 60 * 60; // 24 hours @@ -107,6 +112,7 @@ export async function handleAuthCallback(request: Request, env: GatewayEnv): Pro const tokenData = (await tokenRes.json()) as { access_token?: string; error?: string; + scope?: string; }; if (tokenData.error || !tokenData.access_token) { @@ -118,14 +124,19 @@ export async function handleAuthCallback(request: Request, env: GatewayEnv): Pro // Fetch GitHub user profile const [userRes, emailsRes] = await Promise.all([ fetch(GITHUB_USER_URL, { - headers: { Authorization: `Bearer ${ghToken}`, Accept: "application/vnd.github+json" }, + headers: { ...GITHUB_API_HEADERS, Authorization: `Bearer ${ghToken}` }, }), fetch(GITHUB_EMAILS_URL, { - headers: { Authorization: `Bearer ${ghToken}`, Accept: "application/vnd.github+json" }, + headers: { ...GITHUB_API_HEADERS, Authorization: `Bearer ${ghToken}` }, }), ]); if (!userRes.ok) { + console.error("GitHub /user failed", { + status: userRes.status, + body: (await userRes.text()).slice(0, 500), + scope: tokenData.scope, + }); return errorResponse("Failed to fetch GitHub user", 502); } diff --git a/apps/gateway/test/index.spec.ts b/apps/gateway/test/index.spec.ts index 23e810d..5716473 100644 --- a/apps/gateway/test/index.spec.ts +++ b/apps/gateway/test/index.spec.ts @@ -4,12 +4,39 @@ import { waitOnExecutionContext, SELF, } from "cloudflare:test"; -import { describe, it, expect } from "vitest"; +import { afterEach, describe, it, expect, vi } from "vitest"; +import { handleAuthCallback } from "../src/auth"; import worker from "../src/index"; +import type { GatewayEnv } from "../src/types"; const IncomingRequest = Request; +function testEnv(): GatewayEnv { + const kv = new Map(); + return { + CONTROL_PLANE_URL: "https://control.example", + WS_URL: "wss://ws.example", + GITHUB_OAUTH_CLIENT_ID: "client-id", + GITHUB_OAUTH_CLIENT_SECRET: "client-secret", + INTERNAL_CALLBACK_SECRET: "callback-secret", + APP_JWT_SIGNING_KEY: "test-signing-key-with-enough-length", + GATEWAY_KV: { + get: async (key: string) => kv.get(key) ?? null, + put: async (key: string, value: string) => { + kv.set(key, value); + }, + delete: async (key: string) => { + kv.delete(key); + }, + } as unknown as KVNamespace, + }; +} + describe("Gateway worker", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + it("GET /config returns JSON", async () => { const request = new IncomingRequest("http://example.com/config"); const ctx = createExecutionContext(); @@ -56,4 +83,42 @@ describe("Gateway worker", () => { expect(response.status).toBe(401); expect(await response.json()).toEqual({ error: "Unauthorized" }); }); + + it("sends required GitHub API headers when fetching OAuth user details", async () => { + const authEnv = testEnv(); + await authEnv.GATEWAY_KV.put("pkce:test-state", JSON.stringify({ + verifier: "verifier", + appRedirectUri: "mobile://auth/callback", + })); + + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + const url = typeof input === "string" ? input : input.url; + if (url === "https://github.com/login/oauth/access_token") { + return Response.json({ access_token: "gh-token", scope: "repo,user" }); + } + if (url === "https://api.github.com/user") { + const headers = new Headers(init?.headers); + expect(headers.get("Authorization")).toBe("Bearer gh-token"); + expect(headers.get("Accept")).toBe("application/vnd.github+json"); + expect(headers.get("User-Agent")).toBe("constructor-gateway"); + expect(headers.get("X-GitHub-Api-Version")).toBe("2022-11-28"); + return Response.json({ id: 123, login: "octocat", name: "Octo Cat", email: "octo@example.com" }); + } + if (url === "https://api.github.com/user/emails") { + const headers = new Headers(init?.headers); + expect(headers.get("User-Agent")).toBe("constructor-gateway"); + return Response.json([]); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + + const response = await handleAuthCallback( + new Request("https://gateway.example/auth/callback?code=code&state=test-state"), + authEnv, + ); + + expect(response.status).toBe(302); + expect(response.headers.get("location")).toContain("mobile://auth/callback?token="); + expect(fetchSpy).toHaveBeenCalledTimes(3); + }); });