Desktop sign-in routes an already-verified account to phone ENROLLMENT instead of challenging the existing MFA factor (web succeeds with identical credentials)

Open 💬 4 comments Opened Jul 23, 2026 by culstrup

Summary

The Codex/ChatGPT desktop client for macOS dead-ends at auth.openai.com/add-phone for an account that already has a verified SMS factor. This is not the "SMS will not send" cluster (#25828, #29440). The problem is that the client asks for phone enrollment at all, when it should be issuing a challenge against the factor already on the account.

Environment

  • ChatGPT desktop app for macOS, build 26.715.72359 (bundle id com.openai.codex). Also reproduced on 26.715.31925, and an auto-update between the two did not fix it.
  • macOS 26.5.1 (25F80), Apple Silicon (M3 Max)
  • ChatGPT Business account, Global Admin, US mobile number, no VPN, no adblock, no custom DNS

Controlled A/B that isolates the client

Same account, same machine, same identity provider, same credential, minutes apart:

| Client | Flow | Result |
|---|---|---|
| chatgpt.com (web) | Continue with Google, then passkey | Succeeds. Lands in the existing account with full history. No phone prompt at any point. |
| Desktop app | Continue with Google, same account, same passkey | Passkey succeeds, then redirects to /add-phone and cannot proceed |

Because credential, identity provider, account, and machine are all held constant, password type, MFA factor type, and IdP are eliminated as causes. The only remaining variable is the client.

A network capture of the successful web flow shows the expected sequence:

/api/accounts/password/verify    200
/api/accounts/mfa/issue_challenge 200
/api/accounts/mfa/verify          200
-> OAuth callback -> signed in

The desktop flow never reaches issue_challenge.

Corroborating detail: the client is definitely on an enrollment path

Two different errors at that screen both indicate enrollment rather than challenge:

  1. Entering a VoIP number returns "It looks like this is a virtual phone number (also known as VoIP). Please provide a valid, non-virtual phone number to continue." A challenge against an existing factor would never validate the type of a newly entered number.
  2. Entering the number that is already verified on the account returns "This phone number is already linked to the maximum number of accounts." That is the enrollment path refusing a number it should have been challenging.

Possible mechanism: split identity records after a Google Workspace domain rename

This account's Google Workspace primary domain was renamed, so Google now presents a new-domain address for the same person. On OpenAI the two addresses behave inconsistently, which suggests two distinct auth identity records:

  • New-domain address via Continue with Google: resolves into the existing account and works on web.
  • New-domain address via direct email sign-in: fails with error_code: invalid_auth_step (example request_id: b738c329-62c7-4a5b-b850-2353fc4223ae), and on refresh the flow offers a Create a password screen, i.e. it treats the address as a brand new signup.

So the new-domain address is a Google-linked identity with no standalone account behind it, not an alias of the primary account.

Hypothesis: the desktop client resolves sign-in to an identity record that carries no MFA factor of its own, so it routes to enrollment, which then correctly refuses. The web client resolves through to the primary record, which does have the factor. I offer this as a hypothesis rather than a conclusion, because the failure was also observed with the original-domain address plus password before Google SSO was ever used. Identity resolution may therefore be part of the story rather than all of it.

Expected vs actual

  • Expected: an account with a verified SMS factor receives an MFA challenge against that factor.
  • Actual: the desktop client routes to phone enrollment, enrollment correctly refuses, and there is no path forward.

Impact

Signing in with a ChatGPT account is impossible from the desktop app. The only workaround is API-key auth, which:

  • bills the Platform organization per token on top of an already-paid Business seat,
  • disables cloud tasks, and
  • disables remote control from mobile, because that feature requires account authentication rather than an API key.

Other users on Windows with the same workspace are not affected, which is further evidence this is specific to the macOS desktop client path.

Related

  • #25828 (phone verification cannot send a code)
  • #32657 (desktop verification uses an unknown phone number)
  • #30892 (CLI login blocked, unknown foreign number)
  • #27872 (local auth redirects to add-phone while Cloud/API work, the same shape as this report)

An OpenAI support case is open on this with a full network capture and a screen recording. I am happy to share the case number, account identifiers, and the capture privately with anyone triaging.

View original on GitHub ↗

4 Comments

culstrup · 1 month ago

Update: the client contains no phone-enrollment logic, so this is server-side.

I inspected the installed macOS app directly. Across both Contents/Resources/app.asar (197 MB Electron bundle) and Contents/Resources/codex (265 MB Rust binary):

add-phone            0 occurrences
add_phone            0 occurrences
issue_challenge      0 occurrences
phone_verification   0 occurrences
requiresPhone        0 occurrences

app_EMoamEEZ73f0CkXaXp7hrann   present in both   (Codex/desktop OAuth client)
app_X8zY6vW2pQ9tR3dE7nK1jL5gH  0 occurrences     (web OAuth client)

The desktop client cannot be choosing to route to phone enrollment, because no such route or logic is compiled into it. It authenticates as app_EMoamEEZ73f0CkXaXp7hrann and follows wherever the hosted flow at auth.openai.com sends it. The web client uses a different client id and is not redirected.

Conclusion: auth.openai.com is applying a different phone-verification policy per OAuth client id. This is consistent with the two observations a client-side explanation cannot account for: an auto-update from 26.715.31925 to 26.715.72359 changed nothing, and the identical Google identity plus passkey succeeds in the browser minutes apart.

Practical implication: no app release will fix this. The fix is a server-side policy change scoped to that client id. This likely belongs with whoever owns the auth service rather than the desktop app team.

Reproducible on any stock install:

grep -ao "add-phone" /Applications/ChatGPT.app/Contents/Resources/app.asar | wc -l
grep -ao "app_[A-Za-z0-9]\{20,\}" /Applications/ChatGPT.app/Contents/Resources/app.asar | sort -u
culstrup · 1 month ago

Root cause captured: the server issues add_phone after authentication succeeds, keyed on the destination app

I captured the failing transaction at the network level using CDP response-body capture: two controlled Codex logins in an isolated, freshly created Chrome profile with no cookies, plus a web control in that same profile.

The response that causes it

POST https://auth.openai.com/api/accounts/mfa/verify returns HTTP 200, meaning authentication succeeded. Its body:

{
  "continue_url": "https://auth.openai.com/add-phone",
  "method": "GET",
  "page": { "type": "add_phone" },
  "oai-client-auth-session": {
    "app_name_enum": "oaicli",
    "destination_app_name": "Codex",
    "mfa_challenge_factors": [
      "passkey", "totp", "push_auth", "sms", "recovery_code", "email"
    ]
  }
}

(mfa_challenge_factors abbreviated to its factor_type values; account identifiers removed.)

What this establishes

  1. Authentication fully succeeds before the gate. mfa/verify returns 200 and the passkey is accepted. add_phone is an additional post-authentication gate, not a fallback for failed MFA.
  2. The server selects it, not the client. continue_url and page.type are both server-issued. The client cannot be choosing this: add-phone, add_phone, issue_challenge, phone_verification, and requiresPhone all return 0 occurrences across app.asar and the bundled codex binary.
  3. Identity is not the variable. Session A used Google SSO. Session B used email plus password with a different email address on the same account. Both produced the identical add_phone outcome. Any theory based on identity resolution, SSO linkage, or a renamed domain is ruled out.
  4. The one constant is the destination app: app_name_enum: oaicli, destination_app_name: Codex.

The contradiction

That same response lists sms among the account's registered MFA factors while simultaneously directing the user to add a phone number. The account already has a verified phone factor, and the server knows it.

Entering that already-verified number at /add-phone returns "already linked to the maximum number of accounts" (the documented 3-verifications-per-number reuse cap). So the enrollment being demanded is impossible to complete. That is the deadlock: a gate demands an enrollment that a separate policy forbids.

Control: the web client, same browser profile, minutes later

chatgpt.com signs in normally with no phone gate. The web client performs no OAuth authorize at all (first-party /api/auth/signin/openai) and requests no scopes. That same session then successfully calls /backend-api/aip/connectors/list_accessible, so the account already holds working connector access with zero phone verification.

The likely trigger

The Codex authorize request:

https://auth.openai.com/oauth/authorize
  ?response_type=code
  &client_id=app_EMoamEEZ73f0CkXaXp7hrann
  &scope=openid profile email offline_access api.connectors.read api.connectors.invoke
  &originator=codex_cli_rs
  &id_token_add_organizations=true
  &codex_cli_simplified_flow=true
  &redirect_uri=http://localhost:1455/auth/callback

Codex requests connector API scopes as part of login; the web client requests none. If granting those scopes pulls in a phone-verification requirement, that accounts for every observation here, including why the same account already has connector access through a surface that never asks.

Not a desktop-app bug

This reproduces identically via codex login in the CLI and via the merged ChatGPT/Codex macOS app. Different binaries, same failure. No client release can fix it.

What would fix it

A server-side change: do not apply the add_phone requirement to sessions whose destination app is Codex when the account already has registered MFA factors, matching the web client's behavior.

Unrelated gotcha that cost me hours

Each codex login retry kills the previous localhost:1455 callback listener. Any browser tab from an earlier attempt therefore hangs forever after you submit credentials, because nothing is listening to receive the callback. Only the tab opened by the currently running login can complete. If you are stuck on an endless spinner after entering your password, that is why.

Related open issues with the same shape: #25828, #32657, #30892, #27872.

parthram · 14 days ago

Same issue here on the current ChatGPT macOS desktop app. Web login works normally, but clicking Log in from the desktop app opens Chrome and redirects directly to auth.openai.com/add-phone. Phone verification then says my number is associated with the maximum number of accounts. Fresh reinstall and clearing local app preferences did not help. macOS app version 1.2026.184. Bundle ID appears to be

lerfich · 7 days ago

same same same same...
codex login, .codex removal, codex login --device-auth

all of these leads to same thing.. Anyone, solutions? looks like it would be easier to buy credits rather than creating new account....