{"record":{"id":"1e262f31c746dd6a","repo":"sipeed/picoclaw","slug":"device-code-authentication-timed-out-after-15-minu","errorCode":null,"errorMessage":"device code authentication timed out after 15 minutes","messagePattern":"device code authentication timed out after 15 minutes","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":387,"sourceCode":"\n\tif deviceResp.Interval < 1 {\n\t\tdeviceResp.Interval = 5\n\t}\n\n\tfmt.Printf(\n\t\t\"\\nTo authenticate, open this URL in your browser:\\n\\n  %s/codex/device\\n\\nThen enter this code: %s\\n\\nWaiting for authentication...\\n\",\n\t\tcfg.Issuer,\n\t\tdeviceResp.UserCode,\n\t)\n\n\tdeadline := time.After(15 * time.Minute)\n\tticker := time.NewTicker(time.Duration(deviceResp.Interval) * time.Second)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-deadline:\n\t\t\treturn nil, fmt.Errorf(\"device code authentication timed out after 15 minutes\")\n\t\tcase <-ticker.C:\n\t\t\tcred, err := pollDeviceCode(cfg, deviceResp.DeviceAuthID, deviceResp.UserCode)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif cred != nil {\n\t\t\t\treturn cred, nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc pollDeviceCode(cfg OAuthProviderConfig, deviceAuthID, userCode string) (*AuthCredential, error) {\n\treqBody, _ := json.Marshal(map[string]string{\n\t\t\"device_auth_id\": deviceAuthID,\n\t\t\"user_code\":      userCode,\n\t})\n","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L369-L405","documentation":"LoginDeviceCode (pkg/auth/oauth.go:387) enforces a hard 15-minute deadline on the whole device-authorization flow. time.After(15 * time.Minute) fires before any pollDeviceCode tick returned a credential, so the user never completed browser authentication (or polls kept failing) within the window. The device code itself typically expires on the server at the same timescale.","triggerScenarios":"Starting LoginDeviceCode, not visiting {Issuer}/codex/device and entering the user_code within 15 minutes; or the user approving but polls erroring every tick (poll errors are silently skipped via 'continue'), so success is never observed before the deadline.","commonSituations":"User steps away from the terminal; user code typo'd repeatedly until expiry; slow_interval polling (interval set high by the server) delaying detection of approval; network to the token endpoint broken so every poll fails silently until the deadline.","solutions":["Simply rerun the login flow — a fresh device code and a new 15-minute window is the standard fix","Complete the browser step promptly: the prompt prints the URL and user code as soon as the flow starts","If you need a longer window or programmatic control, use RequestDeviceCode + PollDeviceCodeOnce in your own loop with your own timeout","If the user already approved but login still times out, check network access to {Issuer}/api/accounts/deviceauth/token — poll failures are swallowed and look identical to 'user never approved'","Log pollDeviceCode errors instead of 'continue' so silent polling failures are visible"],"exampleFix":"// before (errors silently swallowed)\ncred, err := pollDeviceCode(cfg, deviceResp.DeviceAuthID, deviceResp.UserCode)\nif err != nil {\n\tcontinue\n}\n\n// after (surface persistent poll failures)\ncred, err := pollDeviceCode(cfg, deviceResp.DeviceAuthID, deviceResp.UserCode)\nif err != nil {\n\tif !strings.Contains(err.Error(), \"pending\") {\n\t\tlog.Printf(\"device code poll error: %v\", err)\n\t}\n\tcontinue\n}","handlingStrategy":"retry","validationCode":"// For custom flows, drive polling yourself with a configurable deadline\ninfo, err := auth.RequestDeviceCode(cfg)\nif err != nil {\n\treturn err\n}\ndeadline := time.Now().Add(20 * time.Minute) // your own window\nfor time.Now().Before(deadline) {\n\tcred, err := auth.PollDeviceCodeOnce(cfg, info.DeviceAuthID, info.UserCode)\n\tif err == nil && cred != nil {\n\t\t_ = cred // authenticated\n\t\tbreak\n\t}\n\ttime.Sleep(time.Duration(info.Interval) * time.Second)\n}","typeGuard":"func isDeviceCodeTimeout(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"timed out after 15 minutes\")\n}","tryCatchPattern":"cred, err := auth.LoginDeviceCode(cfg)\nif err != nil && isDeviceCodeTimeout(err) {\n\t// safe to restart: fresh device code, new window\n\tcred, err = auth.LoginDeviceCode(cfg)\n}\nif err != nil {\n\treturn err\n}","preventionTips":["Tell users to complete browser auth promptly after the code is printed","Use RequestDeviceCode+PollDeviceCodeOnce when 15 minutes is too short","Log non-'pending' poll errors so dead networks don't masquerade as user delay","Restarting the flow is always safe — device codes are single-session"],"tags":["oauth","device-code","timeout","user-interaction","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}