charmbracelet/crush · error

authorization failed: %s

Error message

authorization failed: %s

What it means

tryGetToken maps the token endpoint's "error" field to sentinel errors for known cases (authorization_pending, slow_down); any other error string results in "authorization failed: <code>". This means GitHub rejected the token request with an unexpected OAuth error code such as expired_token, invalid_grant, or unsupported_grant_type.

Source

Thrown at internal/oauth/copilot/oauth.go:145

		AccessToken string `json:"access_token"`
		Error       string `json:"error"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}

	switch result.Error {
	case "":
		if result.AccessToken == "" {
			return nil, errPending
		}
		return getCopilotToken(ctx, result.AccessToken)
	case "authorization_pending":
		return nil, errPending
	case "slow_down":
		return nil, errSlowDown
	default:
		return nil, fmt.Errorf("authorization failed: %s", result.Error)
	}
}

func getCopilotToken(ctx context.Context, githubToken string) (*oauth.Token, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", copilotTokenURL, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", githubToken))
	for k, v := range Headers() {
		req.Header.Set(k, v)
	}

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Restart the device-flow login with a fresh device code — most causes (expired/denied) require it anyway
  2. Log the exact error code in the message to distinguish access_denied from expired_token
  3. If access_denied, ask the user to approve the authorization prompt next time
  4. Handle expired_token by restarting the flow slightly before expiry instead of polling until failure

Example fix

// before
if err != nil { return err }
// after
if err != nil {
    if strings.Contains(err.Error(), "expired_token") {
        dc, _ = RequestDeviceCode(ctx) // restart flow
        return PollForToken(ctx, dc)
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// none: the OAuth error code arrives only during polling

Try / catch

tok, err := PollForToken(ctx, dc)
if err != nil {
    if strings.Contains(err.Error(), "authorization failed: expired_token") {
        dc, _ = RequestDeviceCode(ctx)
        tok, err = PollForToken(ctx, dc)
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: PollForToken receives a 200 response whose JSON error field is not one of the three handled values — most commonly expired_token (device code expired between poll cycles) or access_denied (user denied the request).

Common situations: User clicked "Cancel"/denied authorization in the browser; device code expired just as the user confirmed; GitHub changed its error vocabulary; clock skew affecting expiry handling.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/0d4fedd2488e3f66. Report an issue: GitHub.