{"record":{"id":"c8c740dddf635eed","repo":"BloopAI/vibe-kanban","slug":"github-token-exchange-failed-detail","errorCode":null,"errorMessage":"github token exchange failed: {detail}","messagePattern":"github token exchange failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/remote/src/auth/provider.rs","lineNumber":222,"sourceCode":"        match response.json::<GitHubTokenResponse>().await? {\n            GitHubTokenResponse::Success {\n                access_token,\n                scope,\n                token_type,\n            } => Ok(AuthorizationGrant {\n                access_token: SecretString::new(access_token.into()),\n                token_type,\n                scopes: Self::parse_scopes(scope),\n                refresh_token: None,\n                expires_in: None,\n                id_token: None,\n            }),\n            GitHubTokenResponse::Error {\n                error,\n                error_description,\n            } => {\n                let detail = error_description.unwrap_or_else(|| error.clone());\n                anyhow::bail!(\"github token exchange failed: {detail}\")\n            }\n        }\n    }\n\n    async fn fetch_user(&self, access_token: &SecretString) -> Result<ProviderUser> {\n        let bearer = format!(\"Bearer {}\", access_token.expose_secret());\n\n        let user: GitHubUser = self\n            .client\n            .get(\"https://api.github.com/user\")\n            .header(\"Accept\", \"application/vnd.github+json\")\n            .header(\"Authorization\", &bearer)\n            .send()\n            .await?\n            .error_for_status()?\n            .json()\n            .await?;\n","sourceCodeStart":204,"sourceCodeEnd":240,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/crates/remote/src/auth/provider.rs#L204-L240","documentation":"During the GitHub OAuth code-for-token exchange, GitHub responded with an error variant in its token response (e.g. bad_verification_code, redirect_uri mismatch) instead of an access token. The provider surfaces GitHub's own error (or its description) in the message.","triggerScenarios":"exchange_code() called with an expired, already-used, or invalid authorization code; GitHub returns error/error_description in the token response body.","commonSituations":"User took too long to authorize so the code expired; code replayed on a second exchange; mismatched client_id/client_secret or redirect_uri between the authorize and token requests; clock skew invalidating signed requests.","solutions":["Restart the OAuth flow to obtain a fresh authorization code","Verify client_id, client_secret, and redirect_uri match between the auth URL and token request","Ensure the code is exchanged exactly once and immediately after redirect","Check GitHub OAuth app settings in the developer portal"],"exampleFix":"// before\n// exchanging a code twice\nlet t1 = provider.exchange_code(&code).await?;\nlet t2 = provider.exchange_code(&code).await?; // github token exchange failed: bad_verification_code\n// after\nlet t = provider.exchange_code(&code).await?; // exchange exactly once","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match provider.exchange_code(&code).await { Err(e) if e.to_string().starts_with(\"github token exchange failed\") => restart_oauth_flow(), // codes are single-use; never retry with same code\n other => other? }","preventionTips":["Exchange the code immediately and exactly once","Keep redirect_uri, client_id, client_secret identical across authorize/token steps","Rotate long-lived flows through refresh tokens instead of reusing codes","Match OAuth app settings in the GitHub developer portal"],"tags":["oauth","github","token-exchange","http"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}