fix(openai-codex): request required oauth api scopes (#24720)

This commit is contained in:
Vignesh Natarajan
2026-03-05 18:10:03 -08:00
parent fb289b7a79
commit 8088218f46
3 changed files with 84 additions and 1 deletions

View File

@@ -104,6 +104,42 @@ describe("loginOpenAICodexOAuth", () => {
expect(runtime.error).not.toHaveBeenCalled();
});
it("augments OAuth authorize URL with required OpenAI API scopes", async () => {
const creds = {
provider: "openai-codex" as const,
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
email: "user@example.com",
};
const onAuthSpy = vi.fn();
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
onAuth: onAuthSpy,
onPrompt: vi.fn(),
});
mocks.loginOpenAICodex.mockImplementation(
async (opts: { onAuth: (event: { url: string }) => Promise<void> }) => {
await opts.onAuth({
url: "https://auth.openai.com/oauth/authorize?scope=openid+profile+email+offline_access&state=abc",
});
return creds;
},
);
await runCodexOAuth({ isRemote: false });
expect(onAuthSpy).toHaveBeenCalledTimes(1);
const event = onAuthSpy.mock.calls[0]?.[0] as { url: string };
const scopes = new Set((new URL(event.url).searchParams.get("scope") ?? "").split(/\s+/));
expect(scopes.has("openid")).toBe(true);
expect(scopes.has("profile")).toBe(true);
expect(scopes.has("email")).toBe(true);
expect(scopes.has("offline_access")).toBe(true);
expect(scopes.has("api.responses.write")).toBe(true);
expect(scopes.has("model.request")).toBe(true);
expect(scopes.has("api.model.read")).toBe(true);
});
it("reports oauth errors and rethrows", async () => {
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
onAuth: vi.fn(),

View File

@@ -10,6 +10,46 @@ import {
const OPENAI_RESPONSES_ENDPOINT = "https://api.openai.com/v1/responses";
const OPENAI_RESPONSES_WRITE_SCOPE = "api.responses.write";
const OPENAI_REQUIRED_OAUTH_SCOPES = [
OPENAI_RESPONSES_WRITE_SCOPE,
"model.request",
"api.model.read",
] as const;
function augmentOpenAIOAuthScopes(authUrl: string): string {
try {
const parsed = new URL(authUrl);
const scopeParam = parsed.searchParams.get("scope");
if (!scopeParam) {
return authUrl;
}
const scopes = scopeParam
.split(/\s+/)
.map((scope) => scope.trim())
.filter(Boolean);
if (scopes.length === 0) {
return authUrl;
}
const seen = new Set(scopes.map((scope) => scope.toLowerCase()));
let changed = false;
for (const requiredScope of OPENAI_REQUIRED_OAUTH_SCOPES) {
const normalized = requiredScope.toLowerCase();
if (seen.has(normalized)) {
continue;
}
scopes.push(requiredScope);
seen.add(normalized);
changed = true;
}
if (!changed) {
return authUrl;
}
parsed.searchParams.set("scope", scopes.join(" "));
return parsed.toString();
} catch {
return authUrl;
}
}
function extractResponsesScopeErrorMessage(status: number, bodyText: string): string | null {
if (status !== 401) {
@@ -76,7 +116,7 @@ export async function loginOpenAICodexOAuth(params: {
const spin = prompter.progress("Starting OAuth flow…");
try {
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
const { onAuth: baseOnAuth, onPrompt } = createVpsAwareOAuthHandlers({
isRemote,
prompter,
runtime,
@@ -84,6 +124,12 @@ export async function loginOpenAICodexOAuth(params: {
openUrl,
localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…",
});
const onAuth = async (event: { url: string }) => {
await baseOnAuth({
...event,
url: augmentOpenAIOAuthScopes(event.url),
});
};
const creds = await loginOpenAICodex({
onAuth,