Skip to content

[BUG] Host auth fallback overwrites distinct OAuth users in one Team workspace #230

Description

@proamo

Bug: host auth fallback overwrites different OAuth users in one Team workspace

Environment

  • oc-codex-multi-auth: 6.12.1
  • OpenCode: 1.18.16
  • Node.js: 24.18.0
  • Operating system: Ubuntu 24.04 LTS
  • JSON account backend
  • Multiple OAuth users/seats belonging to the same ChatGPT Team workspace

Problem

Two distinct OAuth users can have different:

  • email
  • OAuth sub
  • chatgpt_user_id
  • refresh token

while sharing the same chatgpt_account_id because they are seats in one Team
workspace.

On plugin startup, AccountState.initializeFromStorage() treats every stored
record with the fallback's accountId as the same credential:

(fallbackAccountId && account.accountId === fallbackAccountId)

The single host OAuth fallback is therefore copied over every seat that shares
the workspace account ID. All affected records end up with the last logged-in
user's email, access token, and refresh token. Subsequent refreshes operate on
the duplicated credential instead of the independent users.

The login persistence path has a related collision. A login can expose several
workspace candidates, and persistAccountPool() resolves an existing record by
organizationId plus accountId. When a different OAuth user can see the same
workspace candidates, those records are updated with the new user's credential.
This means adding another seat can overwrite existing seats before the next
restart.

Reproduction

  1. Authenticate OAuth user A, a seat in Team workspace W.
  2. Authenticate OAuth user B, another seat in the same workspace W.
  3. Confirm A and B have different token subjects and refresh tokens, but the
    same chatgpt_account_id.
  4. Preserve both records in the multi-account file.
  5. Restart OpenCode while the host OAuth fallback points to B.
  6. Observe that A is overwritten with B's email and tokens.

No concurrency is required; the issue reproduces with one OpenCode process.

Expected

An exact refresh-token or email match should hydrate that credential. An
account-ID-only fallback should be used only when the account ID uniquely
identifies one stored credential.

Proposed Fix

Minimal startup fix

Count stored records matching fallbackAccountId. Permit account-ID fallback
only when the count is exactly one. Continue preferring exact refresh-token and
email matches.

The minimal change is confined to dist/lib/accounts/state.js: compute the
number of stored records matching fallbackAccountId, remove the unconditional
account-ID branch from matchesFallback and hasMatchingFallback, and restore
that branch only when the count is exactly one.

Complete identity fix

Persist a credential identity derived from the OAuth sub or
chatgpt_user_id, separate from the request-routing chatgpt_account_id.

  • Credential merge, login update, refresh rotation, and host fallback matching
    should prefer the credential identity.
  • chatgpt_account_id should remain the value sent to the API for workspace
    routing and entitlement evaluation.
  • organizationId should remain workspace metadata, not the sole OAuth-user
    identity.
  • Legacy records without a credential identity can retain the current fallback
    hierarchy, with account-ID matching restricted to unique records.

This distinction is required to make future same-Team-seat login additive
instead of destructive.

Validation

The proposed fix was tested with five accounts, including multiple seats that
share one Team chatgpt_account_id:

  • restart preserved distinct emails, OAuth subjects, and refresh tokens
  • all five refresh-token rotations succeeded
  • the shared-workspace seats remained distinct after rotation
  • all five health checks passed
  • quota queries succeeded for all five accounts

The minimal patch protects already-separated records across startup and refresh.
Adding another shared-workspace seat still requires the complete credential
identity change above or a controlled offline merge.

All identifiers, emails, and token values in this report are intentionally
omitted.

Compliance Checklist

  • I'm using this plugin for personal development only
  • I have an active ChatGPT Plus/Pro subscription
  • This issue is not related to attempting commercial use or TOS violations
  • I've reviewed the FAQ and Troubleshooting sections

The affected users have active ChatGPT Team seats rather than Plus/Pro
subscriptions. No credentials are shared between people; these are separately
authenticated OAuth users managed by the same operator for personal
development.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions