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
- Authenticate OAuth user A, a seat in Team workspace W.
- Authenticate OAuth user B, another seat in the same workspace W.
- Confirm A and B have different token subjects and refresh tokens, but the
same chatgpt_account_id.
- Preserve both records in the multi-account file.
- Restart OpenCode while the host OAuth fallback points to B.
- 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
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.
Bug: host auth fallback overwrites different OAuth users in one Team workspace
Environment
oc-codex-multi-auth: 6.12.1Problem
Two distinct OAuth users can have different:
subchatgpt_user_idwhile sharing the same
chatgpt_account_idbecause they are seats in one Teamworkspace.
On plugin startup,
AccountState.initializeFromStorage()treats every storedrecord with the fallback's
accountIdas the same credential: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 byorganizationIdplusaccountId. When a different OAuth user can see the sameworkspace 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
same
chatgpt_account_id.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 fallbackonly 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 thenumber of stored records matching
fallbackAccountId, remove the unconditionalaccount-ID branch from
matchesFallbackandhasMatchingFallback, and restorethat branch only when the count is exactly one.
Complete identity fix
Persist a credential identity derived from the OAuth
suborchatgpt_user_id, separate from the request-routingchatgpt_account_id.should prefer the credential identity.
chatgpt_account_idshould remain the value sent to the API for workspacerouting and entitlement evaluation.
organizationIdshould remain workspace metadata, not the sole OAuth-useridentity.
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: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
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.