fix(chutes): accept manual OAuth code input

This commit is contained in:
Peter Steinberger
2026-02-14 20:53:39 +01:00
parent c5406e1d24
commit ee8d8be2e3
3 changed files with 11 additions and 7 deletions

View File

@@ -21,10 +21,9 @@ describe("parseOAuthCallbackInput", () => {
expect((result as { error: string }).error).toMatch(/state mismatch/i); expect((result as { error: string }).error).toMatch(/state mismatch/i);
}); });
it("rejects bare code input without fabricating state", () => { it("accepts bare code input for manual flow", () => {
const result = parseOAuthCallbackInput("bare_auth_code", EXPECTED_STATE); const result = parseOAuthCallbackInput("bare_auth_code", EXPECTED_STATE);
expect(result).toHaveProperty("error"); expect(result).toEqual({ code: "bare_auth_code", state: EXPECTED_STATE });
expect(result).not.toHaveProperty("code");
}); });
it("rejects empty input", () => { it("rejects empty input", () => {

View File

@@ -50,14 +50,19 @@ export function parseOAuthCallbackInput(
return { error: "Missing 'code' parameter in URL" }; return { error: "Missing 'code' parameter in URL" };
} }
if (!state) { if (!state) {
return { error: "Missing 'state' parameter. Paste the full URL." }; return { error: "Missing 'state' parameter. Paste the full URL (or just the code)." };
} }
if (state !== expectedState) { if (state !== expectedState) {
return { error: "OAuth state mismatch - possible CSRF attack. Please retry login." }; return { error: "OAuth state mismatch - possible CSRF attack. Please retry login." };
} }
return { code, state }; return { code, state };
} catch { } catch {
return { error: "Paste the full redirect URL, not just the code." }; // Manual flow: users often paste only the authorization code.
// In that case we can't validate state, but the user is explicitly opting in by pasting it.
if (!/\s/.test(trimmed) && !trimmed.includes("://") && trimmed.length > 0) {
return { code: trimmed, state: expectedState };
}
return { error: "Paste the redirect URL (or authorization code)." };
} }
} }

View File

@@ -156,7 +156,7 @@ export async function loginChutes(params: {
await params.onAuth({ url }); await params.onAuth({ url });
params.onProgress?.("Waiting for redirect URL…"); params.onProgress?.("Waiting for redirect URL…");
const input = await params.onPrompt({ const input = await params.onPrompt({
message: "Paste the redirect URL", message: "Paste the redirect URL (or authorization code)",
placeholder: `${params.app.redirectUri}?code=...&state=...`, placeholder: `${params.app.redirectUri}?code=...&state=...`,
}); });
const parsed = parseOAuthCallbackInput(String(input), state); const parsed = parseOAuthCallbackInput(String(input), state);
@@ -176,7 +176,7 @@ export async function loginChutes(params: {
}).catch(async () => { }).catch(async () => {
params.onProgress?.("OAuth callback not detected; paste redirect URL…"); params.onProgress?.("OAuth callback not detected; paste redirect URL…");
const input = await params.onPrompt({ const input = await params.onPrompt({
message: "Paste the redirect URL", message: "Paste the redirect URL (or authorization code)",
placeholder: `${params.app.redirectUri}?code=...&state=...`, placeholder: `${params.app.redirectUri}?code=...&state=...`,
}); });
const parsed = parseOAuthCallbackInput(String(input), state); const parsed = parseOAuthCallbackInput(String(input), state);