import type { Choice } from "./get-api-key-components"; import type { Request, Response } from "express"; import { ApiKeyPrompt, WaitingForAuth } from "./get-api-key-components"; import { clearTerminal } from "./terminal"; import chalk from "chalk"; import express from "express"; import fs from "fs/promises"; import { render } from "ink"; import crypto from "node:crypto"; import { URL } from "node:url"; import open from "open"; import os from "os"; import path from "path"; import React from "react"; function promptUserForChoice(): Promise { return new Promise((resolve) => { const instance = render( { resolve(choice); instance.unmount(); }} />, ); }); } interface OidcConfiguration { issuer: string; authorization_endpoint: string; token_endpoint: string; } async function getOidcConfiguration( issuer: string, ): Promise { const discoveryUrl = new URL(issuer); discoveryUrl.pathname = "/.well-known/openid-configuration"; if (issuer === "https://auth.openai.com") { // Account for legacy quirk in production tenant discoveryUrl.pathname = "/v2.0" + discoveryUrl.pathname; } const res = await fetch(discoveryUrl.toString()); if (!res.ok) { throw new Error("Failed to fetch OIDC configuration"); } return (await res.json()) as OidcConfiguration; } interface IDTokenClaims { "https://api.openai.com/auth": { organization_id: string; project_id: string; completed_platform_onboarding: boolean; is_org_owner: boolean; }; } interface AccessTokenClaims { "https://api.openai.com/auth": { chatgpt_plan_type: string; }; } function generatePKCECodes(): { code_verifier: string; code_challenge: string; } { const code_verifier = crypto.randomBytes(64).toString("hex"); const code_challenge = crypto .createHash("sha256") .update(code_verifier) .digest("base64url"); return { code_verifier, code_challenge }; } async function handleCallback( req: Request, issuer: string, oidcConfig: OidcConfiguration, codeVerifier: string, clientId: string, redirectUri: string, expectedState: string, ): Promise<{ access_token: string; success_url: string }> { const state = (req.query as Record)["state"] as | string | undefined; if (!state || state !== expectedState) { throw new Error("Invalid state parameter"); } const code = (req.query as Record)["code"] as | string | undefined; if (!code) { throw new Error("Missing authorization code"); } const params = new URLSearchParams(); params.append("grant_type", "authorization_code"); params.append("code", code); params.append("redirect_uri", redirectUri); params.append("client_id", clientId); params.append("code_verifier", codeVerifier); oidcConfig.token_endpoint = `${issuer}/oauth/token`; const tokenRes = await fetch(oidcConfig.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: params.toString(), }); if (!tokenRes.ok) { throw new Error("Failed to exchange authorization code for tokens"); } const tokenData = (await tokenRes.json()) as { access_token: string; id_token: string; refresh_token?: string; }; const idTokenParts = tokenData.id_token.split("."); if (idTokenParts.length !== 3) { throw new Error("Invalid ID token"); } const accessTokenParts = tokenData.access_token.split("."); if (accessTokenParts.length !== 3) { throw new Error("Invalid access token"); } const idTokenClaims = JSON.parse( Buffer.from(idTokenParts[1]!, "base64url").toString("utf8"), ) as IDTokenClaims; const accessTokenClaims = JSON.parse( Buffer.from(accessTokenParts[1]!, "base64url").toString("utf8"), ) as AccessTokenClaims; const org_id = idTokenClaims["https://api.openai.com/auth"]?.organization_id; if (!org_id) { throw new Error("Missing organization in id_token claims"); } const project_id = idTokenClaims["https://api.openai.com/auth"]?.project_id; if (!project_id) { throw new Error("Missing project in id_token claims"); } const randomId = crypto.randomBytes(6).toString("hex"); const exchangeParams = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", client_id: clientId, requested_token: "openai-api-key", subject_token: tokenData.id_token, subject_token_type: "urn:ietf:params:oauth:token-type:id_token", name: `Codex CLI [auto-generated] (${new Date().toISOString().slice(0, 10)}) [${ randomId }]`, }); const exchangeRes = await fetch(oidcConfig.token_endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: exchangeParams.toString(), }); if (!exchangeRes.ok) { throw new Error(`Failed to create API key: ${await exchangeRes.text()}`); } const exchanged = (await exchangeRes.json()) as { access_token: string; key: string; }; // Determine whether the organization still requires additional // setup (e.g., adding a payment method) based on the ID-token // claim provided by the auth service. const completedOnboarding = Boolean( idTokenClaims["https://api.openai.com/auth"]?.completed_platform_onboarding, ); const chatgptPlanType = accessTokenClaims["https://api.openai.com/auth"]?.chatgpt_plan_type; const isOrgOwner = Boolean( idTokenClaims["https://api.openai.com/auth"]?.is_org_owner, ); const needsSetup = !completedOnboarding && isOrgOwner; // Build the success URL on the same host/port as the callback and // include the required query parameters for the front-end page. // console.log("Redirecting to success page"); const successUrl = new URL("/success", redirectUri); if (issuer === "https://auth.openai.com") { successUrl.searchParams.set("platform_url", "https://platform.openai.com"); } else { successUrl.searchParams.set( "platform_url", "https://platform.api.openai.org", ); } successUrl.searchParams.set("id_token", tokenData.id_token); successUrl.searchParams.set("needs_setup", needsSetup ? "true" : "false"); successUrl.searchParams.set("org_id", org_id); successUrl.searchParams.set("project_id", project_id); successUrl.searchParams.set("plan_type", chatgptPlanType); try { const home = os.homedir(); const authDir = path.join(home, ".codex"); await fs.mkdir(authDir, { recursive: true }); const authFile = path.join(authDir, "auth.json"); const authData = { tokens: tokenData, last_refresh: new Date().toISOString(), OPENAI_API_KEY: exchanged.access_token, }; await fs.writeFile(authFile, JSON.stringify(authData, null, 2), { mode: 0o600, }); } catch (err) { // eslint-disable-next-line no-console console.warn("Unable to save auth file:", err); } if ( !needsSetup && (chatgptPlanType === "plus" || chatgptPlanType === "pro") ) { const apiHost = issuer === "https://auth.openai.com" ? "https://api.openai.com" : "https://api.openai.org"; try { const redeemRes = await fetch(`${apiHost}/v1/billing/redeem_credits`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ id_token: tokenData.id_token }), }); if (!redeemRes.ok) { // eslint-disable-next-line no-console console.warn( `Credit redemption request failed: ${redeemRes.status} ${redeemRes.statusText}`, ); } else { // Attempt to parse the JSON response and surface a success message try { const redeemData = (await redeemRes.json()) as { granted_chatgpt_subscriber_api_credits?: number; }; const granted = redeemData?.granted_chatgpt_subscriber_api_credits ?? 0; if (granted > 0) { // eslint-disable-next-line no-console console.log( chalk.green( `\u2728 Granted ${chatgptPlanType === "plus" ? "$5" : "$50"} in API credits for being a ChatGPT ${ chatgptPlanType === "plus" ? "Plus" : "Pro" } subscriber!`, ), ); } } catch (parseErr) { // eslint-disable-next-line no-console console.warn("Unable to parse credit redemption response:", parseErr); } } } catch (err) { // eslint-disable-next-line no-console console.warn("Unable to redeem ChatGPT subscriber API credits:", err); } } return { access_token: exchanged.access_token, success_url: successUrl.toString(), }; } const LOGIN_SUCCESS_HTML = String.raw` Sign into Codex CLI
Signed in to Codex CLI
`; async function signInFlow(issuer: string, clientId: string): Promise { const app = express(); let codeVerifier = ""; let redirectUri = ""; let server: ReturnType; const state = crypto.randomBytes(32).toString("hex"); const apiKeyPromise = new Promise((resolve, reject) => { let _apiKey: string | undefined; app.get("/success", (_req: Request, res: Response) => { res.type("text/html").send(LOGIN_SUCCESS_HTML); if (_apiKey) { resolve(_apiKey); } else { // eslint-disable-next-line no-console console.error( "Sorry, it seems like the authentication flow failed. Please try again, or submit an issue on our GitHub if it continues.", ); process.exit(1); } }); // Callback route ------------------------------------------------------- app.get("/auth/callback", async (req: Request, res: Response) => { try { const oidcConfig = await getOidcConfiguration(issuer); oidcConfig.token_endpoint = `${issuer}/oauth/token`; oidcConfig.authorization_endpoint = `${issuer}/oauth/authorize`; const { access_token, success_url } = await handleCallback( req, issuer, oidcConfig, codeVerifier, clientId, redirectUri, state, ); _apiKey = access_token; res.redirect(success_url); } catch (err) { reject(err); } }); server = app.listen(1455, "127.0.0.1", async () => { const address = server.address(); if (typeof address === "string" || !address) { // eslint-disable-next-line no-console console.log( "It seems like you might already be trying to sign in (port :1455 already in use)", ); process.exit(1); return; } const port = address.port; redirectUri = `http://localhost:${port}/auth/callback`; try { const oidcConfig = await getOidcConfiguration(issuer); oidcConfig.token_endpoint = `${issuer}/oauth/token`; oidcConfig.authorization_endpoint = `${issuer}/oauth/authorize`; const pkce = generatePKCECodes(); codeVerifier = pkce.code_verifier; const authUrl = new URL(oidcConfig.authorization_endpoint); authUrl.searchParams.append("response_type", "code"); authUrl.searchParams.append("client_id", clientId); authUrl.searchParams.append("redirect_uri", redirectUri); authUrl.searchParams.append( "scope", "openid profile email offline_access", ); authUrl.searchParams.append("code_challenge", pkce.code_challenge); authUrl.searchParams.append("code_challenge_method", "S256"); authUrl.searchParams.append("id_token_add_organizations", "true"); authUrl.searchParams.append("state", state); // Open the browser immediately. open(authUrl.toString()); setTimeout(() => { // eslint-disable-next-line no-console console.log( `\nOpening login page in your browser: ${authUrl.toString()}\n`, ); }, 500); } catch (err) { reject(err); } }); }); // Ensure the server is closed afterwards. return apiKeyPromise.finally(() => { if (server) { server.close(); } }); } export async function getApiKey( issuer: string, clientId: string, ): Promise { if (process.env["OPENAI_API_KEY"]) { return process.env["OPENAI_API_KEY"]!; } const choice = await promptUserForChoice(); if (choice.type === "apikey") { process.env["OPENAI_API_KEY"] = choice.key; return choice.key; } const spinner = render(); try { const key = await signInFlow(issuer, clientId); spinner.unmount(); clearTerminal(); process.env["OPENAI_API_KEY"] = key; return key; } catch (err) { spinner.unmount(); throw err; } }