Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/server/management/oauth-account-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ function validateKeyName(
}

export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;

if (url.pathname === "/api/accounts/events" && req.method === "GET") {
const { accountSelectionStream } = await import("./account-selection-stream");
Expand All @@ -172,6 +172,17 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
const provider = (body.provider ?? "").trim().toLowerCase();
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
// Meta Muse login imports a credential from the user's macOS Keychain and
// persists it in OpenCodex. A raw management token proves administrative
// access, not that a person acknowledged that credential move and its ToS
// risk. The dashboard warning therefore needs this matching server-side gate;
// headers are not evidence because an admin-token holder can forge them.
if (provider === "meta-muse" && principal !== "gui-session") {
return jsonResponse({
error: "Meta Muse import requires acknowledgement in the OpenCodex dashboard.",
code: "oauth_consent_required",
}, 403);
}
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
if (namespaceCollision) return jsonResponse({ error: namespaceCollision }, 409);
const accountId = body.accountId?.trim();
Expand Down
29 changes: 29 additions & 0 deletions tests/oauth/oauth-public-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,35 @@ describe("legacy ChatGPT OAuth public-surface exclusion", () => {
expect(isPublicOAuthProvider("github-copilot")).toBe(true);
});

test("Meta Muse import requires a consent-bearing GUI session", async () => {
const cfg = config();
const request = () => new Request("http://localhost/api/oauth/login", {
method: "POST",
headers: {
"content-type": "application/json",
origin: "http://localhost",
"x-opencodex-gui-origin": "http://localhost",
"x-opencodex-csrf-token": "forgeable-without-a-session",
},
// A missing account makes a correctly admitted request stop before the
// platform-specific import, while still proving it passed the consent gate.
body: JSON.stringify({ provider: "meta-muse", accountId: "missing-slot" }),
});

for (const principal of [undefined, "admin-token", "gui-pair-capability"] as const) {
const response = await handleManagementAPI(request(), new URL(request().url), cfg, {}, principal);
expect(response?.status).toBe(403);
expect(await response?.json()).toEqual({
error: "Meta Muse import requires acknowledgement in the OpenCodex dashboard.",
code: "oauth_consent_required",
});
}

const admitted = await handleManagementAPI(request(), new URL(request().url), cfg, {}, "gui-session");
expect(admitted?.status).toBe(404);
expect(await admitted?.json()).toEqual({ error: "Unknown account for reauth" });
});

test("generic management OAuth endpoints reject chatgpt before touching login state", async () => {
const cfg = config();
const requests = [
Expand Down
Loading