{"record":{"id":"fefea06efc12db98","repo":"sipeed/picoclaw","slug":"state-mismatch-fefea0","errorCode":null,"errorMessage":"State mismatch","messagePattern":"State mismatch","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":196,"sourceCode":"\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}\n","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L178-L214","documentation":"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.","triggerScenarios":"(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.","commonSituations":"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.","solutions":["Start a fresh authorize URL from the app for every attempt — never reuse or bookmark login links","Ensure only one auth flow runs at a time per port/profile","Retry the login end-to-end (new state is generated automatically)","If it persists, check that no other local process is competing for the callback port"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"token, err := auth.Login(ctx)\nif err != nil {\n    if strings.Contains(err.Error(), \"state mismatch\") {\n        // stale callback (old tab/bookmark) or concurrent flow — a fresh attempt gets a new state\n        token, err = auth.Login(ctx)\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["Always start login from a freshly issued authorize URL; never reuse bookmarked login links","Serialize logins — one in-flight flow per profile/port at a time","On restart, discard browser state from the previous session instead of resuming its consent page"],"tags":["oauth","auth","security","csrf"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}