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

  1. Simply rerun the login flow — a fresh device code and a new 15-minute window is the standard fix
  2. Complete the browser step promptly: the prompt prints the URL and user code as soon as the flow starts
  3. If you need a longer window or programmatic control, use RequestDeviceCode + PollDeviceCodeOnce in your own loop with your own timeout
  4. 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'
  5. 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

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

Related errors


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