fix: OpenRouter OAuth denial redirects show provider errors (#105448)

* fix(openrouter): surface pasted OAuth redirect errors

Co-authored-by: chatgpt-codex-connector[bot] <261883814+chatgpt-codex-connector[bot]@users.noreply.github.com>

* fix(openrouter): align callback OAuth error state checks

Co-authored-by: chatgpt-codex-connector[bot] <261883814+chatgpt-codex-connector[bot]@users.noreply.github.com>

* test(openrouter): initialize callback readiness

---------

Co-authored-by: chatgpt-codex-connector[bot] <261883814+chatgpt-codex-connector[bot]@users.noreply.github.com>
Co-authored-by: Altay <altay@hey.com>
This commit is contained in:
VectorPeak
2026-07-13 20:37:26 +08:00
committed by GitHub
parent 9f3d3b5672
commit 79cc3c333b
2 changed files with 94 additions and 16 deletions
+68
View File
@@ -138,6 +138,27 @@ function createOpenRouterOAuthContext(params: {
return { ctx, progress, note, text, log, openUrl };
}
async function requestLocalOpenRouterOAuthCallback(
query: string,
): Promise<{ callback: Promise<unknown>; response: Response; body: string }> {
let markReady = () => {};
const ready = new Promise<void>((resolve) => {
markReady = resolve;
});
const callback = waitForOpenRouterOAuthCallback({
expectedState: "state-1",
timeoutMs: 1000,
onProgress: markReady,
});
callback.catch(() => undefined);
await ready;
const response = await fetch(`${OPENROUTER_OAUTH_REDIRECT_URI}?${query}`, {
headers: { Connection: "close" },
});
return { callback, response, body: await response.text() };
}
describe("OpenRouter OAuth", () => {
it("builds the documented PKCE authorize URL", () => {
const url = new URL(
@@ -167,6 +188,30 @@ describe("OpenRouter OAuth", () => {
code: "AUTHCODE",
state: "state-1",
});
expect(() =>
parseOpenRouterOAuthCallbackInput(
`${OPENROUTER_OAUTH_REDIRECT_URI}?state=state-1&error=access_denied&error_description=Denied`,
"state-1",
),
).toThrow("OpenRouter OAuth error: access_denied: Denied");
expect(() =>
parseOpenRouterOAuthCallbackInput(
"state=state-1&error=access_denied&error_description=Denied",
"state-1",
),
).toThrow("OpenRouter OAuth error: access_denied: Denied");
expect(() =>
parseOpenRouterOAuthCallbackInput(
`${OPENROUTER_OAUTH_REDIRECT_URI}?error=access_denied&error_description=Denied`,
"state-1",
),
).toThrow("Missing OpenRouter OAuth state");
expect(() =>
parseOpenRouterOAuthCallbackInput(
`${OPENROUTER_OAUTH_REDIRECT_URI}?state=wrong&error=access_denied&error_description=Denied`,
"state-1",
),
).toThrow("OpenRouter OAuth state mismatch");
expect(buildOpenRouterOAuthRedirectUri({ state: "state-1" })).toBe(
`${OPENROUTER_OAUTH_REDIRECT_URI}?state=state-1`,
);
@@ -348,6 +393,29 @@ describe("OpenRouter OAuth", () => {
expect(text).not.toHaveBeenCalled();
});
it("validates local callback state before surfacing OpenRouter OAuth errors", async () => {
const denied = await requestLocalOpenRouterOAuthCallback(
"state=state-1&error=access_denied&error_description=Denied",
);
expect(denied.response.status).toBe(400);
expect(denied.body).toBe("OpenRouter authentication failed: access_denied: Denied");
await expect(denied.callback).rejects.toThrow("OpenRouter OAuth error: access_denied: Denied");
const missingState = await requestLocalOpenRouterOAuthCallback(
"error=access_denied&error_description=Denied",
);
expect(missingState.response.status).toBe(400);
expect(missingState.body).toBe("Invalid OAuth state");
await expect(missingState.callback).rejects.toThrow("Missing OpenRouter OAuth state");
const wrongState = await requestLocalOpenRouterOAuthCallback(
"state=wrong&error=access_denied&error_description=Denied",
);
expect(wrongState.response.status).toBe(400);
expect(wrongState.body).toBe("Invalid OAuth state");
await expect(wrongState.callback).rejects.toThrow("OpenRouter OAuth state mismatch");
});
it("exposes stable auth choice metadata", () => {
expect(OPENROUTER_OAUTH_CHOICE_ID).toBe("openrouter-oauth");
});
+26 -16
View File
@@ -143,11 +143,18 @@ export function parseOpenRouterOAuthCallbackInput(
}
const parseParams = (params: URLSearchParams): OpenRouterOAuthCallbackResult => {
const state = requireOpenRouterOAuthState(readString(params.get("state")), expectedState);
const error = readString(params.get("error"));
if (error) {
const description = readString(params.get("error_description"));
throw new Error(
`OpenRouter OAuth error: ${description ? `${error}: ${description}` : error}`,
);
}
const code = readString(params.get("code"));
if (!code) {
throw new Error("Missing 'code' parameter in redirect URL.");
}
const state = requireOpenRouterOAuthState(readString(params.get("state")), expectedState);
return { code, state };
};
@@ -156,7 +163,7 @@ export function parseOpenRouterOAuthCallbackInput(
return parseParams(url.searchParams);
} catch (err) {
if (err instanceof TypeError) {
if (trimmed.includes("code=")) {
if (trimmed.includes("code=") || trimmed.includes("error=")) {
return parseParams(new URLSearchParams(trimmed));
}
throw new Error("Paste the full OpenRouter redirect URL, not just the code.", {
@@ -231,12 +238,25 @@ export async function waitForOpenRouterOAuthCallback(params: {
return;
}
const error = readString(requestUrl.searchParams.get("error"));
if (error) {
const state = readString(requestUrl.searchParams.get("state"));
try {
requireOpenRouterOAuthState(state, params.expectedState);
} catch (err) {
res.statusCode = 400;
res.setHeader("Content-Type", "text/plain");
res.end(`OpenRouter authentication failed: ${error}`);
finish(new Error(`OpenRouter OAuth error: ${error}`));
res.end("Invalid OAuth state");
finish(err instanceof Error ? err : new Error("OpenRouter OAuth state mismatch"));
return;
}
const error = readString(requestUrl.searchParams.get("error"));
if (error) {
const description = readString(requestUrl.searchParams.get("error_description"));
const detail = description ? `${error}: ${description}` : error;
res.statusCode = 400;
res.setHeader("Content-Type", "text/plain");
res.end(`OpenRouter authentication failed: ${detail}`);
finish(new Error(`OpenRouter OAuth error: ${detail}`));
return;
}
@@ -248,16 +268,6 @@ export async function waitForOpenRouterOAuthCallback(params: {
finish(new Error("Missing OpenRouter OAuth code"));
return;
}
const state = readString(requestUrl.searchParams.get("state"));
try {
requireOpenRouterOAuthState(state, params.expectedState);
} catch (err) {
res.statusCode = 400;
res.setHeader("Content-Type", "text/plain");
res.end("Invalid OAuth state");
finish(err instanceof Error ? err : new Error("OpenRouter OAuth state mismatch"));
return;
}
res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8");