Add Codex OAuth provider flow for ChatGPT account sign-in (#503)

* feat: add Codex OAuth provider flow

* fix: harden Codex OAuth storage, session activation, and UI
This commit is contained in:
Henrique Fernandes
2026-04-13 11:34:16 -03:00
committed by GitHub
parent 252808bbd0
commit fc7dc9ca0d
34 changed files with 5187 additions and 508 deletions

View File

@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { DEFAULT_CODEX_BASE_URL } from '../services/api/providerConfig.ts'
import {
buildStartupEnvFromProfile,
buildAtomicChatProfileEnv,
@@ -12,7 +13,9 @@ import {
buildLaunchEnv,
buildOllamaProfileEnv,
buildOpenAIProfileEnv,
clearPersistedCodexOAuthProfile,
createProfileFile,
isPersistedCodexOAuthProfile,
maskSecretForDisplay,
loadProfileFile,
PROFILE_FILE_NAME,
@@ -23,6 +26,13 @@ import {
type ProfileFile,
} from './providerProfile.ts'
function makeJwt(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`
}
function profile(profile: ProfileFile['profile'], env: ProfileFile['env']): ProfileFile {
return {
profile,
@@ -330,6 +340,7 @@ test('codex profiles accept explicit codex credentials', () => {
assert.deepEqual(env, {
OPENAI_BASE_URL: 'https://chatgpt.com/backend-api/codex',
OPENAI_MODEL: 'codexspark',
CODEX_CREDENTIAL_SOURCE: 'existing',
CODEX_API_KEY: 'codex-live',
CHATGPT_ACCOUNT_ID: 'acct_123',
})
@@ -417,6 +428,77 @@ test('saveProfileFile writes a profile that loadProfileFile can read back', () =
}
})
test('buildCodexProfileEnv tags OAuth-saved profiles so logout can remove them safely', () => {
const env = buildCodexProfileEnv({
model: 'codexplan',
apiKey: makeJwt({
'https://api.openai.com/auth': {
chatgpt_account_id: 'acct_oauth',
},
}),
credentialSource: 'oauth',
processEnv: {},
})
assert.deepEqual(env, {
OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL,
OPENAI_MODEL: 'codexplan',
CODEX_CREDENTIAL_SOURCE: 'oauth',
CODEX_API_KEY: makeJwt({
'https://api.openai.com/auth': {
chatgpt_account_id: 'acct_oauth',
},
}),
CHATGPT_ACCOUNT_ID: 'acct_oauth',
})
})
test('clearPersistedCodexOAuthProfile removes only persisted Codex OAuth profiles', async () => {
const cwd = mkdtempSync(join(tmpdir(), 'openclaude-codex-oauth-profile-'))
try {
const providerProfileModule = await import(
`./providerProfile.ts?ts=${Date.now()}-${Math.random()}`
)
const {
PROFILE_FILE_NAME,
clearPersistedCodexOAuthProfile,
createProfileFile,
isPersistedCodexOAuthProfile,
loadProfileFile,
saveProfileFile,
} = providerProfileModule
const oauthProfile = createProfileFile('codex', {
OPENAI_MODEL: 'codexplan',
OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL,
CHATGPT_ACCOUNT_ID: 'acct_oauth',
CODEX_CREDENTIAL_SOURCE: 'oauth',
})
saveProfileFile(oauthProfile, { cwd })
assert.equal(isPersistedCodexOAuthProfile(loadProfileFile({ cwd })), true)
assert.equal(
clearPersistedCodexOAuthProfile({ cwd }),
join(cwd, PROFILE_FILE_NAME),
)
assert.equal(loadProfileFile({ cwd }), null)
const existingCredentialProfile = createProfileFile('codex', {
OPENAI_MODEL: 'codexplan',
OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL,
CHATGPT_ACCOUNT_ID: 'acct_existing',
CODEX_CREDENTIAL_SOURCE: 'existing',
})
saveProfileFile(existingCredentialProfile, { cwd })
assert.equal(isPersistedCodexOAuthProfile(loadProfileFile({ cwd })), false)
assert.equal(clearPersistedCodexOAuthProfile({ cwd }), null)
assert.deepEqual(loadProfileFile({ cwd }), existingCredentialProfile)
} finally {
rmSync(cwd, { recursive: true, force: true })
}
})
test('buildStartupEnvFromProfile applies persisted gemini settings when no provider is explicitly selected', async () => {
const env = await buildStartupEnvFromProfile({
persisted: profile('gemini', {