feat(auth): support browser login for managed ChatGPT accounts (#8288)

Some managed ChatGPT workspaces disable device-code authentication, so those accounts could not be added to CC Switch. Add browser login (authorization code + PKCE with a loopback callback on port 1455) to the Auth Center and the Codex provider form, including reauthentication; device-code login stays available in a method selector.

Fixes #8284
This commit is contained in:
Xiangji authored and GitHub committed 2026-10-24 21:23:10 +08:00
1 parent bd50984e05
commit c72c82cfbe
14 files changed
+1072 -51

No files matched your search

@@ -324,6 +324,52 @@ describe("CodexOAuthSection", () => {
expect(add).toHaveAttribute("aria-describedby", "auth-pending-chatgpt");
});
it("reopens browser login without showing device-code controls", async () => {
const { settingsApi } = await import("@/lib/api");
const openExternal = vi
.spyOn(settingsApi, "openExternal")
.mockResolvedValue();
const authResult = mocks.useCodexOauth();
mocks.useCodexOauth.mockReturnValue({
...authResult,
pollingState: "polling",
isPolling: true,
isAddingAccount: true,
deviceCode: {
provider: "codex_oauth",
device_code: "browser:flow",
user_code: "",
verification_uri:
"https://auth.openai.com/oauth/authorize?state=fixture",
expires_in: 900,
interval: 1,
},
});
try {
render(<CodexOAuthSection />);
expect(
screen.getByText(/已在浏览器打开 ChatGPT 的登录页/),
).toBeInTheDocument();
expect(
screen.queryByText("在浏览器中输入验证码:"),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "复制验证码" }),
).not.toBeInTheDocument();
expect(
screen.getAllByRole("button", { name: "重新打开登录页" }),
).toHaveLength(1);
await userEvent.click(
screen.getByRole("button", { name: "重新打开登录页" }),
);
expect(openExternal).toHaveBeenCalledWith(
"https://auth.openai.com/oauth/authorize?state=fixture",
);
} finally {
openExternal.mockRestore();
}
});
it("selects a specific account when multiple accounts are managed", async () => {
const user = userEvent.setup();
const onAccountSelect = vi.fn();