{"record":{"id":"60fa8068b82e4d4d","repo":"sipeed/picoclaw","slug":"state-mismatch","errorCode":null,"errorMessage":"state mismatch","messagePattern":"state mismatch","errorType":"exception","errorClass":null,"httpStatus":400,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":195,"sourceCode":"\t\t}\n\t\tif code == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"could not find authorization code in input\")\n\t\t}\n\t\treturn ExchangeCodeForTokens(cfg, code, pkce.CodeVerifier, redirectURI)\n\tcase <-time.After(5 * time.Minute):\n\t\treturn nil, fmt.Errorf(\"authentication timed out after 5 minutes\")\n\t}\n}\n\nfunc oauthCallbackRedirectURI(port int) string {\n\treturn fmt.Sprintf(\"http://localhost:%d/auth/callback\", port)\n}\n\nfunc oauthCallbackHandler(state string, resultCh chan<- callbackResult) http.Handler {\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/auth/callback\", func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Query().Get(\"state\") != state {\n\t\t\tresultCh <- callbackResult{err: fmt.Errorf(\"state mismatch\")}\n\t\t\thttp.Error(w, \"State mismatch\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tcode := r.URL.Query().Get(\"code\")\n\t\tif code == \"\" {\n\t\t\terrMsg := r.URL.Query().Get(\"error\")\n\t\t\tresultCh <- callbackResult{err: fmt.Errorf(\"no code received: %s\", errMsg)}\n\t\t\thttp.Error(w, \"No authorization code received\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"text/html\")\n\t\tfmt.Fprint(w, \"<html><body><h2>Authentication successful!</h2><p>You can close this window.</p></body></html>\")\n\t\tresultCh <- callbackResult{code: code}\n\t})\n\treturn mux\n}","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L177-L213","documentation":"The OAuth callback arrived with a state query parameter that did not match the state generated at flow start (oauth.go:195). This CSRF guard exists to bind the callback to this login attempt. Mismatches come from stale browser tabs replaying an old callback, two concurrent logins sharing the port, or (rarely) forged requests probing the loopback listener.","triggerScenarios":"An old auth page left open from a previous run redirects after a new LoginBrowser started on the same port; running two logins concurrently so each server receives the other's callback; scripted/manual replay of a captured callback URL.","commonSituations":"User retries login while the previous browser tab is still mid-flow; browser restoring pinned tabs that re-fire old redirects; automated tests hitting the callback handler with a canned state.","solutions":["Close all stale authorization tabs from earlier attempts, then re-run login once","Ensure only one login flow is active at a time (one process, one port)","Never re-paste or bookmark the localhost callback URL — it is single-use and state-bound","On mismatch, always restart the flow; the code exchange cannot proceed with mismatched state"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"state mismatch\") {\n    // stale tab or concurrent login: never exchange, always restart\n    fmt.Println(\"stale OAuth callback (state mismatch) — close old auth tabs and re-run login\")\n    return err\n}","preventionTips":["Close authorization tabs from previous attempts before re-logging in","Run exactly one login flow per port at a time","Never replay or bookmark the localhost callback URL"],"tags":["oauth","security","csrf","login","concurrency"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}