Created
June 11, 2026 10:55
-
-
Save kowyo/67c25df2f0f463778b7bcbd27b61b8e8 to your computer and use it in GitHub Desktop.
Minimal OpenAI Codex OAuth PKCE flow demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import http from "node:http"; | |
| import crypto from "node:crypto"; | |
| const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"; | |
| const REDIRECT_URI = "http://localhost:1455/auth/callback"; | |
| const TOKEN_URL = "https://auth.openai.com/oauth/token"; | |
| async function generatePKCE() { | |
| const verifier = crypto.randomBytes(32).toString("base64url"); | |
| const challenge = crypto.createHash("sha256").update(verifier).digest("base64url"); | |
| return { verifier, challenge }; | |
| } | |
| function startCallbackServer(state: string): Promise<string> { | |
| return new Promise((resolve, reject) => { | |
| const server = http.createServer((req, res) => { | |
| const url = new URL(req.url!, "http://localhost"); | |
| if (url.pathname !== "/auth/callback") { res.writeHead(404).end(); return; } | |
| if (url.searchParams.get("state") !== state) { res.writeHead(400).end(); return; } | |
| const code = url.searchParams.get("code"); | |
| if (!code) { res.writeHead(400).end(); return; } | |
| res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }) | |
| .end("<h1>登录成功,可以关闭此窗口</h1>"); | |
| server.close(); | |
| resolve(code); | |
| }); | |
| server.listen(1455, "127.0.0.1"); | |
| server.on("error", (err: NodeJS.ErrnoException) => { | |
| if (err.code === "EADDRINUSE") reject(new Error("端口 1455 被占用")); | |
| else reject(err); | |
| }); | |
| }); | |
| } | |
| async function exchangeCode(code: string, verifier: string) { | |
| const resp = await fetch(TOKEN_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, | |
| body: new URLSearchParams({ | |
| grant_type: "authorization_code", | |
| client_id: CLIENT_ID, code, code_verifier: verifier, redirect_uri: REDIRECT_URI, | |
| }), | |
| }); | |
| if (!resp.ok) throw new Error(`Token exchange failed (${resp.status})`); | |
| return resp.json() as Promise<{ access_token: string; refresh_token: string; expires_in: number }>; | |
| } | |
| function extractAccountId(accessToken: string): string | null { | |
| try { | |
| const payload = JSON.parse(Buffer.from(accessToken.split(".")[1]!, "base64").toString()); | |
| return payload?.["https://api.openai.com/auth"]?.chatgpt_account_id ?? null; | |
| } catch { return null; } | |
| } | |
| async function main() { | |
| const { verifier, challenge } = await generatePKCE(); | |
| const state = crypto.randomBytes(16).toString("hex"); | |
| const authUrl = new URL("https://auth.openai.com/oauth/authorize"); | |
| authUrl.searchParams.set("response_type", "code"); | |
| authUrl.searchParams.set("client_id", CLIENT_ID); | |
| authUrl.searchParams.set("redirect_uri", REDIRECT_URI); | |
| authUrl.searchParams.set("scope", "openid profile email offline_access"); | |
| authUrl.searchParams.set("code_challenge", challenge); | |
| authUrl.searchParams.set("code_challenge_method", "S256"); | |
| authUrl.searchParams.set("state", state); | |
| authUrl.searchParams.set("codex_cli_simplified_flow", "true"); | |
| console.log("\n=== OpenAI Codex OAuth ===\n"); | |
| const codePromise = startCallbackServer(state); | |
| const open = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; | |
| Bun.spawnSync([open, authUrl.toString()]); | |
| console.log(`如果浏览器未自动打开:\n ${authUrl}\n`); | |
| const code = await codePromise; | |
| const token = await exchangeCode(code, verifier); | |
| const accountId = extractAccountId(token.access_token); | |
| console.log(`accountId: ${accountId ?? "提取失败"}`); | |
| console.log(`access_token: ${token.access_token}`); | |
| console.log(`refresh_token: ${token.refresh_token}`); | |
| console.log(`expires_in: ${token.expires_in}s (${(token.expires_in / 3600).toFixed(1)}h)`); | |
| console.log("\n验证 token..."); | |
| const testResp = await fetch("https://chatgpt.com/backend-api/models", { | |
| headers: { Authorization: `Bearer ${token.access_token}`, "chatgpt-account-id": accountId ?? "" }, | |
| }); | |
| console.log(` -> ${testResp.status} ${testResp.statusText}`); | |
| if (testResp.ok) { | |
| const data = await testResp.json() as { models?: Array<{ slug: string }> }; | |
| console.log(`可用模型: ${(data.models ?? []).map((m) => m.slug).join(", ")}`); | |
| } | |
| console.log("\n完成。保存好 access_token / refresh_token / accountId 后续使用。"); | |
| } | |
| main().catch((err) => { console.error(err.message); process.exit(1); }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment