Skip to content

Commit 02cbf60

Browse files
fix(auth): omit empty account id and pin auth header boundaries (#1064)
1 parent 85529dc commit 02cbf60

2 files changed

Lines changed: 103 additions & 2 deletions

File tree

‎src/auth/codex/auth-headers.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export function codexAuthHeadersForToken(
1212
originator: CODEX_ORIGINATOR,
1313
"user-agent": `${commandName} (${CODEX_ORIGINATOR}/${CODEX_CLIENT_VERSION})`,
1414
};
15-
if (token.accountId !== undefined)
16-
headers["chatgpt-account-id"] = token.accountId;
15+
// An empty account id carries no identity — sending it as a header value
16+
// would label the request with a meaningless id. Only a non-empty id rides.
17+
if (token.accountId) headers["chatgpt-account-id"] = token.accountId;
1718
return headers;
1819
}

‎src/auth/oauth-scope-check.test.ts‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, describe, expect, test } from "bun:test";
22

33
import { checkOAuthProviderScope } from "./oauth-scope-check.js";
4+
import { XAI_BASE_URL } from "./xai/constants.js";
45

56
const commandName = "test-cli";
67

@@ -119,6 +120,51 @@ describe("checkOAuthProviderScope", () => {
119120
}
120121
});
121122

123+
test("codex: empty account id omits the account header", async () => {
124+
stubFetch((_url, init) => {
125+
const headers = init?.headers as Record<string, string>;
126+
expect(headers.authorization).toBe("Bearer staged-codex-token");
127+
expect("chatgpt-account-id" in headers).toBe(false);
128+
return new Response(JSON.stringify({ models: ["gpt-5"] }), {
129+
status: 200,
130+
});
131+
});
132+
const result = await checkOAuthProviderScope(
133+
"codex",
134+
{ ...codexTokens, accountId: "" },
135+
commandName,
136+
);
137+
expect(result.status).toBe("ok");
138+
});
139+
140+
test("codex: fail-closed on empty access (Bearer probe classifies blocked)", async () => {
141+
stubFetch((_url, init) => {
142+
const headers = init?.headers as Record<string, string>;
143+
expect(headers.authorization).toBe("Bearer ");
144+
return new Response("nope", { status: 401 });
145+
});
146+
const result = await checkOAuthProviderScope(
147+
"codex",
148+
{ ...codexTokens, access: "" },
149+
commandName,
150+
);
151+
expect(result.status).toBe("blocked");
152+
});
153+
154+
test("codex: fail-closed on garbage access (Bearer probe classifies blocked)", async () => {
155+
stubFetch((_url, init) => {
156+
const headers = init?.headers as Record<string, string>;
157+
expect(headers.authorization).toBe("Bearer !!!not-a-token!!!");
158+
return new Response("forbidden", { status: 403 });
159+
});
160+
const result = await checkOAuthProviderScope(
161+
"codex",
162+
{ ...codexTokens, access: "!!!not-a-token!!!" },
163+
commandName,
164+
);
165+
expect(result.status).toBe("blocked");
166+
});
167+
122168
test("codex: blocks a definitive 401", async () => {
123169
stubFetch(() => new Response("nope", { status: 401 }));
124170
const result = await checkOAuthProviderScope(
@@ -220,6 +266,60 @@ describe("checkOAuthProviderScope", () => {
220266
expect(result.status).toBe("blocked");
221267
});
222268

269+
test("xai: non-JWT token omits the user-id header, keeps authorization", async () => {
270+
stubFetch((_url, init) => {
271+
const headers = init?.headers as Record<string, string>;
272+
expect(headers.authorization).toBe("Bearer not-a-jwt");
273+
expect("x-grok-user-id" in headers).toBe(false);
274+
return new Response(JSON.stringify({ data: [] }), { status: 200 });
275+
});
276+
const result = await checkOAuthProviderScope(
277+
"xai",
278+
{ ...xaiTokens, access: "not-a-jwt" },
279+
commandName,
280+
);
281+
expect(result.status).toBe("ok");
282+
});
283+
284+
test("xai: fail-closed on empty access (Bearer probe classifies blocked)", async () => {
285+
stubFetch((_url, init) => {
286+
const headers = init?.headers as Record<string, string>;
287+
expect(headers.authorization).toBe("Bearer ");
288+
return new Response("nope", { status: 401 });
289+
});
290+
const result = await checkOAuthProviderScope(
291+
"xai",
292+
{ ...xaiTokens, access: "" },
293+
commandName,
294+
);
295+
expect(result.status).toBe("blocked");
296+
});
297+
298+
test("xai: fail-closed on garbage access (Bearer probe classifies blocked)", async () => {
299+
stubFetch((_url, init) => {
300+
const headers = init?.headers as Record<string, string>;
301+
expect(headers.authorization).toBe("Bearer !!!not-a-token!!!");
302+
return new Response("forbidden", { status: 403 });
303+
});
304+
const result = await checkOAuthProviderScope(
305+
"xai",
306+
{ ...xaiTokens, access: "!!!not-a-token!!!" },
307+
commandName,
308+
);
309+
expect(result.status).toBe("blocked");
310+
});
311+
312+
test("xai: pins the probe to the fixed models URL", async () => {
313+
const seen: string[] = [];
314+
stubFetch((url) => {
315+
seen.push(url);
316+
return new Response(JSON.stringify({ data: [] }), { status: 200 });
317+
});
318+
const result = await checkOAuthProviderScope("xai", xaiTokens, commandName);
319+
expect(result.status).toBe("ok");
320+
expect(seen).toEqual([`${XAI_BASE_URL}/models`]);
321+
});
322+
223323
test("xai: unavailable on a timeout-style abort", async () => {
224324
stubFetch(() => {
225325
throw new DOMException("The operation timed out.", "TimeoutError");

0 commit comments

Comments
 (0)