chatboxai/chatbox · error · Error

OAuth state mismatch

Error message

OAuth state mismatch

What it means

Thrown by OpenAI's callback OAuth provider after the local callback server (port 1455) receives a redirect whose state parameter does not equal the random state generated at login start. This is the standard OAuth CSRF defense: a mismatch means the callback did not correspond to this login attempt.

Source

Thrown at src/main/oauth/providers/openai.ts:78

    authUrl.searchParams.set('response_type', 'code')
    authUrl.searchParams.set('client_id', CLIENT_ID)
    authUrl.searchParams.set('redirect_uri', REDIRECT_URI)
    authUrl.searchParams.set('scope', SCOPE)
    authUrl.searchParams.set('code_challenge', challenge)
    authUrl.searchParams.set('code_challenge_method', 'S256')
    authUrl.searchParams.set('state', state)
    authUrl.searchParams.set('id_token_add_organizations', 'true')
    authUrl.searchParams.set('codex_cli_simplified_flow', 'true')
    authUrl.searchParams.set('originator', 'chatbox')

    const { promise, close } = createCallbackServer(CALLBACK_PORT, signal, CALLBACK_HOST)

    try {
      await openUrl(authUrl.toString())
      const result = await promise

      if (result.state !== state) {
        throw new Error('OAuth state mismatch')
      }

      return await exchangeCodeForTokens(result.code, verifier)
    } finally {
      close()
    }
  },

  async refreshToken(credentials) {
    if (!credentials.refreshToken) {
      throw new Error('No refresh token available')
    }

    const response = await fetch(TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'refresh_token',

View on GitHub (pinned to 81571269ad)

Solutions

  1. Treat as a hard security failure: do not exchange the code; restart login() from scratch.
  2. Ensure only one login() is in flight at a time per provider (serialize or cancel prior flows).
  3. Close stale browser tabs on the authorize page before starting a new login.
  4. Confirm createCallbackServer is bound to localhost only so external callers cannot inject callbacks.

Example fix

// before
if (result.state !== state) {
  throw new Error('OAuth state mismatch')
}

// after
if (result.state !== state) {
  log.error('[OAuth:OpenAI] state mismatch — possible stale or forged callback')
  throw new SecurityError('Login state mismatch. Close other ChatGPT login tabs and try again.')
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return await provider.login({ openUrl, signal })
} catch (e) {
  if (/state mismatch/i.test(String(e))) {
  // security: never exchange a mismatched callback; restart fresh
  throw new SecurityError('Login state mismatch. Close other ChatGPT login tabs and retry.')
  }
  throw e
}

Prevention

When it happens

Trigger: Browser was already on a stale authorize page from a previous login and redirected with an old state; a second login tab raced the first and the wrong callback arrived; user manually visited localhost:1455 with a crafted/old URL; an attacker injected a callback (the very attack the guard prevents).

Common situations: User had multiple ChatGPT login tabs open; a previous login's browser tab redirected after a new login started; redirects cached by the browser; testing tools replaying an old callback URL.

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/450ea1cebdbcd0b0. Report an issue: GitHub.