mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 04:46:16 +00:00
fix: decode codex jwt payloads as base64url
This commit is contained in:
@@ -45,6 +45,19 @@ describe("resolveCodexAuthIdentity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("decodes URL-safe base64 JWT payloads", () => {
|
||||
const accessToken = createJwt({
|
||||
"https://api.openai.com/auth": {
|
||||
chatgpt_account_id: "w_ébé_1fzcswWN6Pi5zL",
|
||||
},
|
||||
});
|
||||
expect(accessToken.split(".")[1]).toContain("_");
|
||||
|
||||
expect(resolveCodexAuthIdentity({ accessToken })).toEqual({
|
||||
accountId: "w_ébé_1fzcswWN6Pi5zL",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to credential email before synthetic ids", () => {
|
||||
const identity = resolveCodexAuthIdentity({
|
||||
accessToken: createJwt({}),
|
||||
|
||||
@@ -17,6 +17,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
|
||||
});
|
||||
}
|
||||
|
||||
import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js";
|
||||
import { oauthErrorHtml, oauthSuccessHtml } from "./openai-codex-oauth-page.runtime.js";
|
||||
import type {
|
||||
OAuthCredentials,
|
||||
@@ -32,7 +33,6 @@ const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
||||
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
||||
const REDIRECT_URI = "http://localhost:1455/auth/callback";
|
||||
const SCOPE = "openid profile email offline_access";
|
||||
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
||||
|
||||
type TokenSuccess = { type: "success"; access: string; refresh: string; expires: number };
|
||||
type TokenFailure = { type: "failed"; message: string; status?: number };
|
||||
@@ -43,13 +43,6 @@ type TokenResponseJson = {
|
||||
expires_in?: number;
|
||||
};
|
||||
|
||||
type JwtPayload = {
|
||||
[JWT_CLAIM_PATH]?: {
|
||||
chatgpt_account_id?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function createState(): string {
|
||||
if (!randomBytes) {
|
||||
throw new Error("OpenAI Codex OAuth is only available in Node.js environments");
|
||||
@@ -89,20 +82,6 @@ function parseAuthorizationInput(input: string): { code?: string; state?: string
|
||||
return { code: value };
|
||||
}
|
||||
|
||||
function decodeJwt(token: string): JwtPayload | null {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
const payload = parts[1] ?? "";
|
||||
const decoded = atob(payload);
|
||||
return JSON.parse(decoded) as JwtPayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatMissingTokenResponseFields(json: TokenResponseJson): string {
|
||||
const missing: string[] = [];
|
||||
if (!json.access_token) {
|
||||
@@ -310,9 +289,7 @@ function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
|
||||
}
|
||||
|
||||
function getAccountId(accessToken: string): string | null {
|
||||
const payload = decodeJwt(accessToken);
|
||||
const auth = payload?.[JWT_CLAIM_PATH];
|
||||
const accountId = auth?.chatgpt_account_id;
|
||||
const accountId = resolveCodexAuthIdentity({ accessToken }).accountId;
|
||||
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
||||
}
|
||||
|
||||
|
||||
27
src/llm/providers/openai-codex-responses.test.ts
Normal file
27
src/llm/providers/openai-codex-responses.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractOpenAICodexAccountId } from "./openai-codex-responses.js";
|
||||
|
||||
function createJwt(payload: Record<string, unknown>): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||
return `${header}.${body}.signature`;
|
||||
}
|
||||
|
||||
describe("extractOpenAICodexAccountId", () => {
|
||||
it("decodes URL-safe base64 JWT payloads", () => {
|
||||
const accessToken = createJwt({
|
||||
"https://api.openai.com/auth": {
|
||||
chatgpt_account_id: "w_ébé_1fzcswWN6Pi5zL",
|
||||
},
|
||||
});
|
||||
expect(accessToken.split(".")[1]).toContain("_");
|
||||
|
||||
expect(extractOpenAICodexAccountId(accessToken)).toBe("w_ébé_1fzcswWN6Pi5zL");
|
||||
});
|
||||
|
||||
it("rejects tokens without a Codex account id", () => {
|
||||
expect(() => extractOpenAICodexAccountId(createJwt({}))).toThrow(
|
||||
"Failed to extract accountId from token",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -106,6 +106,12 @@ interface RequestBody {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
type CodexJwtPayload = {
|
||||
[JWT_CLAIM_PATH]?: {
|
||||
chatgpt_account_id?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Retry Helpers
|
||||
// ============================================================================
|
||||
@@ -172,7 +178,7 @@ export const streamOpenAICodexResponses: StreamFunction<
|
||||
throw new Error(`No API key for provider: ${model.provider}`);
|
||||
}
|
||||
|
||||
const accountId = extractAccountId(apiKey);
|
||||
const accountId = extractOpenAICodexAccountId(apiKey);
|
||||
let body = buildRequestBody(model, context, options);
|
||||
const nextBody = await options?.onPayload?.(body, model);
|
||||
if (nextBody !== undefined) {
|
||||
@@ -1458,21 +1464,29 @@ async function parseErrorResponse(
|
||||
// Auth & Headers
|
||||
// ============================================================================
|
||||
|
||||
function extractAccountId(token: string): string {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
|
||||
if (!accountId) {
|
||||
throw new Error("No account ID in token");
|
||||
}
|
||||
return accountId;
|
||||
} catch {
|
||||
throw new Error("Failed to extract accountId from token");
|
||||
function decodeCodexJwtPayload(token: string): CodexJwtPayload | null {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1] ?? "", "base64url").toString("utf8");
|
||||
const parsed = JSON.parse(decoded);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? (parsed as CodexJwtPayload)
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractOpenAICodexAccountId(token: string): string {
|
||||
const accountId = decodeCodexJwtPayload(token)?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
|
||||
if (typeof accountId === "string" && accountId.length > 0) {
|
||||
return accountId;
|
||||
}
|
||||
throw new Error("Failed to extract accountId from token");
|
||||
}
|
||||
|
||||
function createCodexRequestId(): string {
|
||||
|
||||
Reference in New Issue
Block a user