Skip to content

[Bug]: Codex pool allows duplicate registration of native main account #7398

Closed

Description

@terrytan95

Client or integration

OpenCodex dashboard

Area

Authentication and account pool

Summary

When adding or reauthenticating a Codex account in the managed pool via OAuth in the dashboard or CLI, OpenCodex allows registering a pool account that has the exact same ChatGPT account ID, subscription plan bucket, and email as the native Codex main account (__main__ in ~/.codex/auth.json).

Once added, the dashboard displays this duplicate account alongside the native main account. Selecting this pool account or allowing the account pool rotation to select it subsequently dispatches inference requests using that credential, exhausting the native main account's quota under the impression that an alternate account is being utilized.

In addition, extractEmail in src/oauth/chatgpt-shared.ts only looks for top-level payload.email, omitting OpenAI's namespaced payload["https://api.openai.com/profile"]?.email. As a result, email extraction fails during OAuth token exchange for access tokens, causing pool accounts to record alias IDs rather than real account emails, which blinds email-based collision prevention.

Reproduction

  1. Log in to Codex CLI natively with Account A (e.g. main@example.com, Team/Plus subscription), which populates ~/.codex/auth.json with account_id (e.g. 746bcddc-...).
  2. Start OpenCodex: ocx start --port 10100.
  3. Open the OpenCodex dashboard (http://localhost:10100) and navigate to Codex Auth.
  4. Click Add Account to initiate OAuth login.
  5. In the browser popup/tab, if the browser is currently signed into Account A, OpenAI completes OAuth authorization and issues tokens for Account A.
  6. OpenCodex imports this credential into config.codexAccounts and codex-accounts.json under a new pool ID (e.g. account-2).
  7. Observe: OpenCodex accepts Account A into the pool without flagging a duplicate collision with the native main account.
  8. Pin or select account-2 in OpenCodex and send prompts through Codex CLI.
  9. Observe: The native main account's 5-hour and weekly quota are drained because account-2 is an identical clone of __main__.

Expected behavior:
OpenCodex should detect that the newly authorized credentials match the native Codex main account's ChatGPT account ID, plan bucket, and email, and reject registration with a clear error: Account is already configured as the native Codex main account.

Root cause analysis

  1. checkAccountIdCollision skips native main account checks (src/codex/auth-collision.ts):

    • Currently, checkAccountIdCollision only iterates over config.codexAccounts:
      export function checkAccountIdCollision(
        chatgptAccountId: string,
        email?: string | null,
        plan?: unknown,
        excludeAccountId?: string | null,
      ): { collision: true; reason: string } | { collision: false } {
        const candidateEmail = normalizedEmail(email);
        const candidateWorkspace = isWorkspacePlan(plan);
        for (const account of loadConfig().codexAccounts ?? []) {
          // only checks other pool accounts
          ...
        }
        return { collision: false };
      }
    • In commit 0da08f9588, the check mainAccountId === chatgptAccountId was removed to allow users to hold separate personal and business plan buckets for the same ChatGPT account ID, and to permit different team members to share a workspace account ID.
    • However, removing the check entirely created a blind spot: when a pool candidate matches __main__ in the same plan bucket with the same email, it is mistakenly treated as a distinct account.
  2. Access token email extraction defect (src/oauth/chatgpt-shared.ts):

    • extractEmail only parses top-level payload.email:
      export function extractEmail(idToken?: string, accessToken?: string): string | undefined {
        for (const token of [idToken, accessToken]) {
          if (!token) continue;
          const payload = decodeJwtPayload(token);
          if (!payload) continue;
          if (typeof payload.email === "string") return payload.email.toLowerCase();
        }
        return undefined;
      }
    • In OpenAI OAuth access tokens, the email claim is located in payload["https://api.openai.com/profile"]?.email.
    • Because extractEmail returns undefined, login-flow.ts falls back to email = cred.email || accountId. When WHAM usage observation is delayed or does not supply an email, the candidate's email becomes its local ID (e.g. chatgpt-1792445816119), obscuring the account identity and blinding email-based collision prevention.

Scope and distinction from legitimate multi-workspace accounts

It is important to distinguish this defect from legitimate multi-workspace and multi-member setups, which must continue to work:

  • Same user across different workspaces (e.g., user@example.com belonging to Workspace A cfe7a0e6-..., Workspace B 89e44725-..., etc.): Each workspace has a distinct chatgptAccountId and independent quota. These must NOT collide.
  • Different users sharing the same team workspace (e.g., alice@company.com and bob@company.com sharing 89e44725-...): Different emails in the same workspace. These must NOT collide.
  • Duplicate of native main account: Identical chatgptAccountId, identical plan bucket (personal vs workspace), and identical email (or missing email). This MUST be blocked.

Proposed resolution

  1. Extend checkAccountIdCollision in src/codex/auth-collision.ts to inspect the native main account (readCodexTokens()):
    • Compare candidate against native main account's chatgptAccountId and plan bucket (isWorkspacePlan).
    • If both match and emails match (or either email is absent), return { collision: true, reason: "Account is already configured as the native Codex main account." }.
    • Skip checking the main account when excludeAccountId === MAIN_CODEX_ACCOUNT_ID.
  2. Update extractEmail in src/oauth/chatgpt-shared.ts to inspect payload["https://api.openai.com/profile"]?.email in addition to top-level payload.email.
  3. Add regression tests covering:
    • Rejection of pool accounts matching the native main login in the same plan bucket and email.
    • Allowance of different workspace members sharing the main login's chatgptAccountId.
    • Exemption when __main__ is explicitly excluded (reauth).
    • Profile-claim email parsing from access tokens.

Pull request fixing this issue: #7399

Version

2.89.0 (commit d3dafd3)

Operating system

macOS 15.5

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. github-actions commented on Oct 19, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

  2. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    on Oct 19, 2026
  3. Ingwannu commented on Oct 20, 2026

    @Ingwannu
    Owner

    Checked against trusted dev 12025981e5a304cdb6b1b54f525ba8554f623977. The reported admission gap and namespaced profile-email parsing are now addressed by merged #7414 (the current dev tip). auth-collision.ts compares the lifecycle-published native identity while preserving separate workspace/personal buckets and distinct members; chatgpt-shared.ts reads the namespaced profile email. The integration also excludes pre-existing native clones rather than only blocking new admissions. Closing as fixed on dev, not as released or live-account certified. The independent pool-pin/fallback policy question remains #7410.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions