fix(openai): tighten codex device auth trust

This commit is contained in:
Vincent Koc
2026-04-21 12:44:39 -07:00
committed by Val Alexander
parent fd6e12f051
commit cb353c0c5a
6 changed files with 4 additions and 45 deletions

View File

@@ -1,8 +1,5 @@
import { describe, expect, it } from "vitest";
import {
resolveCodexAuthIdentity,
resolveCodexChatgptAccountId,
} from "./openai-codex-auth-identity.js";
import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js";
function createJwt(payload: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
@@ -57,21 +54,3 @@ describe("resolveCodexAuthIdentity", () => {
expect(resolveCodexAuthIdentity({ accessToken: "not-a-jwt-token" })).toEqual({});
});
});
describe("resolveCodexChatgptAccountId", () => {
it("extracts the ChatGPT account id from the auth claim", () => {
expect(
resolveCodexChatgptAccountId(
createJwt({
"https://api.openai.com/auth": {
chatgpt_account_id: "acct_123",
},
}),
),
).toBe("acct_123");
});
it("returns undefined when the account id is missing", () => {
expect(resolveCodexChatgptAccountId(createJwt({}))).toBeUndefined();
});
});

View File

@@ -60,11 +60,6 @@ export function resolveCodexStableSubject(payload: CodexJwtPayload | null): stri
return sub;
}
export function resolveCodexChatgptAccountId(token: string): string | undefined {
const auth = decodeCodexJwtPayload(token)?.["https://api.openai.com/auth"];
return trimNonEmptyString(auth?.chatgpt_account_id);
}
export function resolveCodexAccessTokenExpiry(accessToken: string): number | undefined {
const payload = decodeCodexJwtPayload(accessToken);
const exp = normalizeFutureEpochSeconds(payload?.exp);

View File

@@ -91,8 +91,8 @@ describe("loginOpenAICodexDeviceCode", () => {
expect(credentials).toMatchObject({
access: expect.any(String),
refresh: "refresh-token-123",
accountId: "acct_123",
});
expect(credentials).not.toHaveProperty("accountId");
expect(credentials.expires).toBeGreaterThan(Date.now());
} finally {
vi.useRealTimers();

View File

@@ -1,8 +1,4 @@
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
import {
resolveCodexAccessTokenExpiry,
resolveCodexChatgptAccountId,
} from "./openai-codex-auth-identity.js";
import { resolveCodexAccessTokenExpiry } from "./openai-codex-auth-identity.js";
import { trimNonEmptyString } from "./openai-codex-shared.js";
const OPENAI_AUTH_BASE_URL = "https://auth.openai.com";
@@ -22,7 +18,6 @@ type OpenAICodexDeviceCodeCredentials = {
access: string;
refresh: string;
expires: number;
accountId?: string;
};
type DeviceCodeUserCodePayload = {
@@ -41,7 +36,6 @@ type DeviceCodeTokenPayload = {
type OAuthTokenPayload = {
access_token?: unknown;
refresh_token?: unknown;
id_token?: unknown;
expires_in?: unknown;
};
@@ -259,7 +253,6 @@ async function exchangeOpenAICodexDeviceCode(params: {
const body = parseJsonObject(bodyText) as OAuthTokenPayload | null;
const access = trimNonEmptyString(body?.access_token);
const refresh = trimNonEmptyString(body?.refresh_token);
const idToken = trimNonEmptyString(body?.id_token);
if (!access || !refresh) {
throw new Error("OpenAI token exchange succeeded but did not return OAuth tokens.");
}
@@ -269,14 +262,11 @@ async function exchangeOpenAICodexDeviceCode(params: {
expiresInMs !== undefined
? Date.now() + expiresInMs
: (resolveCodexAccessTokenExpiry(access) ?? Date.now());
const accountId =
resolveCodexChatgptAccountId(access) ?? (idToken && resolveCodexChatgptAccountId(idToken));
return {
access,
refresh,
expires,
...(accountId ? { accountId } : {}),
};
}
@@ -285,7 +275,6 @@ export async function loginOpenAICodexDeviceCode(params: {
onVerification: (prompt: OpenAICodexDeviceCodePrompt) => Promise<void> | void;
onProgress?: (message: string) => void;
}): Promise<OpenAICodexDeviceCodeCredentials> {
ensureGlobalUndiciEnvProxyDispatcher();
const fetchFn = params.fetchFn ?? fetch;
params.onProgress?.("Requesting device code…");

View File

@@ -252,7 +252,6 @@ describe("openai codex provider", () => {
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdC1kZXZpY2UtMTIzIn19.signature",
refresh: "device-refresh-token",
expires: Date.now() + 60_000,
accountId: "acct-device-123",
});
const result = await deviceCodeMethod?.run({
@@ -283,13 +282,13 @@ describe("openai codex provider", () => {
access:
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdC1kZXZpY2UtMTIzIn19.signature",
refresh: "device-refresh-token",
accountId: "acct-device-123",
},
},
],
defaultModel: "openai-codex/gpt-5.4",
});
expect(result?.profiles[0]?.credential).not.toHaveProperty("idToken");
expect(result?.profiles[0]?.credential).not.toHaveProperty("accountId");
});
it("does not log the device pairing code in remote mode", async () => {
@@ -313,7 +312,6 @@ describe("openai codex provider", () => {
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdC1kZXZpY2UtMTIzIn19.signature",
refresh: "device-refresh-token",
expires: Date.now() + 60_000,
accountId: "acct-device-123",
};
});

View File

@@ -30,7 +30,6 @@ import {
readOpenAICodexCliOAuthProfile,
} from "./openai-codex-cli-auth.js";
import { loginOpenAICodexDeviceCode } from "./openai-codex-device-code.js";
import { trimNonEmptyString } from "./openai-codex-shared.js";
import {
buildOpenAIResponsesProviderHooks,
buildOpenAISyntheticCatalogEntry,
@@ -357,7 +356,6 @@ async function runOpenAICodexDeviceCode(ctx: ProviderAuthContext) {
expires: creds.expires,
email: identity.email,
profileName: identity.profileName,
credentialExtra: trimNonEmptyString(creds.accountId) ? { accountId: creds.accountId } : {},
});
} catch (error) {
spin.stop("OpenAI device code failed");