sipeed/picoclaw · error
device code authentication timed out after 15 minutes
Error message
device code authentication timed out after 15 minutes
What it means
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.
Source
Thrown at pkg/auth/oauth.go:387
if deviceResp.Interval < 1 {
deviceResp.Interval = 5
}
fmt.Printf(
"\nTo authenticate, open this URL in your browser:\n\n %s/codex/device\n\nThen enter this code: %s\n\nWaiting for authentication...\n",
cfg.Issuer,
deviceResp.UserCode,
)
deadline := time.After(15 * time.Minute)
ticker := time.NewTicker(time.Duration(deviceResp.Interval) * time.Second)
defer ticker.Stop()
for {
select {
case <-deadline:
return nil, fmt.Errorf("device code authentication timed out after 15 minutes")
case <-ticker.C:
cred, err := pollDeviceCode(cfg, deviceResp.DeviceAuthID, deviceResp.UserCode)
if err != nil {
continue
}
if cred != nil {
return cred, nil
}
}
}
}
func pollDeviceCode(cfg OAuthProviderConfig, deviceAuthID, userCode string) (*AuthCredential, error) {
reqBody, _ := json.Marshal(map[string]string{
"device_auth_id": deviceAuthID,
"user_code": userCode,
})
View on GitHub (pinned to 49183d7e8d)
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
Example fix
// before (errors silently swallowed)
cred, err := pollDeviceCode(cfg, deviceResp.DeviceAuthID, deviceResp.UserCode)
if err != nil {
continue
}
// after (surface persistent poll failures)
cred, err := pollDeviceCode(cfg, deviceResp.DeviceAuthID, deviceResp.UserCode)
if err != nil {
if !strings.Contains(err.Error(), "pending") {
log.Printf("device code poll error: %v", err)
}
continue
} Defensive patterns
Strategy: retry
Validate before calling
// For custom flows, drive polling yourself with a configurable deadline
info, err := auth.RequestDeviceCode(cfg)
if err != nil {
return err
}
deadline := time.Now().Add(20 * time.Minute) // your own window
for time.Now().Before(deadline) {
cred, err := auth.PollDeviceCodeOnce(cfg, info.DeviceAuthID, info.UserCode)
if err == nil && cred != nil {
_ = cred // authenticated
break
}
time.Sleep(time.Duration(info.Interval) * time.Second)
} Type guard
func isDeviceCodeTimeout(err error) bool {
return err != nil && strings.Contains(err.Error(), "timed out after 15 minutes")
} Try / catch
cred, err := auth.LoginDeviceCode(cfg)
if err != nil && isDeviceCodeTimeout(err) {
// safe to restart: fresh device code, new window
cred, err = auth.LoginDeviceCode(cfg)
}
if err != nil {
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- parsing device code response: %w
- reading device token response: %w
- error fetching models: %w
- WeCom QR scan timed out after %s
- request failed: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/1e262f31c746dd6a.
Report an issue: GitHub.