zeroclaw-labs/zeroclaw · error · anyhow::Error

GitHub device authorization expired

Error message

GitHub device authorization expired

What it means

GitHub's OAuth device flow returns expired_token when the user_code shown at github.com/login/device was not confirmed within expires_in (typically about 15 minutes). The poll loop in device_code_login aborts instead of continuing to poll a dead code, so login must be restarted.

Source

Thrown at crates/zeroclaw-providers/src/copilot.rs:600

                    "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
                }))
                .send()
                .await?
                .json()
                .await?;

            if let Some(token) = token_response.access_token {
                eprintln!("Authentication succeeded.\n");
                return Ok(token);
            }

            match token_response.error.as_deref() {
                Some("slow_down") => {
                    poll_interval += Duration::from_secs(5);
                }
                Some("authorization_pending") | None => {}
                Some("expired_token") => {
                    anyhow::bail!("GitHub device authorization expired")
                }
                Some(error) => anyhow::bail!("GitHub auth failed: {error}"),
            }
        }

        anyhow::bail!("Timed out waiting for GitHub authorization")
    }

    /// Exchange a GitHub access token for a Copilot API key.
    async fn exchange_for_api_key(&self, access_token: &str) -> anyhow::Result<ApiKeyInfo> {
        let mut request = self.http_client().get(GITHUB_API_KEY_URL);
        for (header, value) in &Self::COPILOT_HEADERS {
            request = request.header(*header, *value);
        }
        request = request.header("Authorization", format!("token {access_token}"));

        let response = request.send().await?;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Re-run the Copilot login and complete the browser prompt within the window (~15 minutes)
  2. Copy the user_code exactly from the terminal when prompted at github.com/login/device
  3. For headless machines, authenticate once elsewhere and copy the token dir, or configure a GitHub token in config instead of device flow
  4. Keep the terminal session alive - killing it discards the pending device code
Defensive patterns

Strategy: retry

Try / catch

const MAX_ATTEMPTS: usize = 3;
for attempt in 1..=MAX_ATTEMPTS {
    match copilot_login().await {
        Ok(token) => break_ok(token),
        Err(e) if e.to_string().contains("device authorization expired") => {
            if attempt == MAX_ATTEMPTS { return Err(e); }
            continue; // fresh device code, user gets a new window
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: device_code_login polls GITHUB_ACCESS_TOKEN_URL; the response error field is expired_token because nobody entered and approved the code in the browser before the deadline. Only raised during first-time Copilot login (cached tokens skip the flow).

Common situations: Unattended or headless login left waiting; user steps away from the browser; slow relaying of the code to the user; reusing a stale login attempt.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/307cc83bffb94971. Report an issue: GitHub.