{"record":{"id":"0d4fedd2488e3f66","repo":"charmbracelet/crush","slug":"authorization-failed-s","errorCode":null,"errorMessage":"authorization failed: %s","messagePattern":"authorization failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/oauth/copilot/oauth.go","lineNumber":145,"sourceCode":"\t\tAccessToken string `json:\"access_token\"`\n\t\tError       string `json:\"error\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch result.Error {\n\tcase \"\":\n\t\tif result.AccessToken == \"\" {\n\t\t\treturn nil, errPending\n\t\t}\n\t\treturn getCopilotToken(ctx, result.AccessToken)\n\tcase \"authorization_pending\":\n\t\treturn nil, errPending\n\tcase \"slow_down\":\n\t\treturn nil, errSlowDown\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"authorization failed: %s\", result.Error)\n\t}\n}\n\nfunc getCopilotToken(ctx context.Context, githubToken string) (*oauth.Token, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", copilotTokenURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", githubToken))\n\tfor k, v := range Headers() {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/oauth/copilot/oauth.go#L127-L163","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Restart the device-flow login with a fresh device code — most causes (expired/denied) require it anyway","Log the exact error code in the message to distinguish access_denied from expired_token","If access_denied, ask the user to approve the authorization prompt next time","Handle expired_token by restarting the flow slightly before expiry instead of polling until failure"],"exampleFix":"// before\nif err != nil { return err }\n// after\nif err != nil {\n    if strings.Contains(err.Error(), \"expired_token\") {\n        dc, _ = RequestDeviceCode(ctx) // restart flow\n        return PollForToken(ctx, dc)\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// none: the OAuth error code arrives only during polling\n","typeGuard":null,"tryCatchPattern":"tok, err := PollForToken(ctx, dc)\nif err != nil {\n    if strings.Contains(err.Error(), \"authorization failed: expired_token\") {\n        dc, _ = RequestDeviceCode(ctx)\n        tok, err = PollForToken(ctx, dc)\n    }\n    if err != nil { return err }\n}","preventionTips":["Restart the flow on expired_token; do not keep polling a dead code","Distinguish access_denied (user UX issue) from expired_token (timing) in logs","Stop polling as soon as the code's expires_in elapses"],"tags":["oauth","device-flow","copilot","authorization"],"backgroundTag":"oauth-authorization-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}