sipeed/picoclaw · error

State mismatch

Error message

State mismatch

What it means

During OAuth login the library starts a localhost callback server (127.0.0.1) and compares the state query parameter of the /auth/callback request against the randomly generated state issued in the authorize URL. A mismatch fails this check: the callback does not correspond to the current authorization request, which is the standard CSRF/replay protection for the authorization code flow.

Source

Thrown at pkg/auth/oauth.go:196

		if code == "" {
			return nil, fmt.Errorf("could not find authorization code in input")
		}
		return ExchangeCodeForTokens(cfg, code, pkce.CodeVerifier, redirectURI)
	case <-time.After(5 * time.Minute):
		return nil, fmt.Errorf("authentication timed out after 5 minutes")
	}
}

func oauthCallbackRedirectURI(port int) string {
	return fmt.Sprintf("http://localhost:%d/auth/callback", port)
}

func oauthCallbackHandler(state string, resultCh chan<- callbackResult) http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Query().Get("state") != state {
			resultCh <- callbackResult{err: fmt.Errorf("state mismatch")}
			http.Error(w, "State mismatch", http.StatusBadRequest)
			return
		}

		code := r.URL.Query().Get("code")
		if code == "" {
			errMsg := r.URL.Query().Get("error")
			resultCh <- callbackResult{err: fmt.Errorf("no code received: %s", errMsg)}
			http.Error(w, "No authorization code received", http.StatusBadRequest)
			return
		}

		w.Header().Set("Content-Type", "text/html")
		fmt.Fprint(w, "<html><body><h2>Authentication successful!</h2><p>You can close this window.</p></body></html>")
		resultCh <- callbackResult{code: code}
	})
	return mux
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Start a fresh authorize URL from the app for every attempt — never reuse or bookmark login links
  2. Ensure only one auth flow runs at a time per port/profile
  3. Retry the login end-to-end (new state is generated automatically)
  4. If it persists, check that no other local process is competing for the callback port
Defensive patterns

Strategy: retry

Try / catch

token, err := auth.Login(ctx)
if err != nil {
    if strings.Contains(err.Error(), "state mismatch") {
        // stale callback (old tab/bookmark) or concurrent flow — a fresh attempt gets a new state
        token, err = auth.Login(ctx)
    }
    if err != nil {
        return err
    }
}

Prevention

When it happens

Trigger: (1) Reusing an old authorize URL or reloaded callback page after the flow restarted (state rotates per attempt); (2) two concurrent logins — the other flow's callback (different state) hits this listener; (3) port reuse: a previous flow's listener closed and the port was handed to a new flow while the browser still had the old tab; (4) an unrelated/crafted request probing the callback URL.

Common situations: User refreshes the callback page or reuses a bookmarked login link; multiple tabs/tokens initiating auth at once; CLI restarted mid-flow while the browser sat on the consent page; security scanners hitting localhost callback endpoints.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/fefea06efc12db98. Report an issue: GitHub.