From 52dad56462f5ef45065d8e9e13d85464f2016be9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 27 Feb 2026 16:37:17 -0500 Subject: [PATCH] fix(coderd): refresh OAuth token before GitHub API calls in chat diff (#22415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `resolveChatGitHubAccessToken` reads the `OAuthAccessToken` directly from the database without refreshing it. When the token expires, GitHub returns "bad credentials" and the chat diff features break. ## Fix Call `config.RefreshToken()` before returning the token — the same code path used by `provisionerdserver` when handing tokens to provisioners. - Builds a map of provider ID → `*externalauth.Config` during the existing config iteration - After fetching the `ExternalAuthLink` from the DB, calls `cfg.RefreshToken()` if a matching config exists - On refresh failure, falls through to the existing token (GitHub tokens without expiry still work) with a debug log --- coderd/chats.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/coderd/chats.go b/coderd/chats.go index b30e62c2b5..5fcbd84fb7 100644 --- a/coderd/chats.go +++ b/coderd/chats.go @@ -26,6 +26,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpapi/httperror" "github.com/coder/coder/v2/coderd/httpmw" @@ -1442,6 +1443,9 @@ func (api *API) resolveChatGitHubAccessToken( ctx context.Context, userID uuid.UUID, ) string { + // Build a map of provider ID -> config so we can refresh tokens + // using the same code path as provisionerdserver. + ghConfigs := make(map[string]*externalauth.Config) providerIDs := []string{"github"} for _, config := range api.ExternalAuthConfigs { if !strings.EqualFold( @@ -1451,6 +1455,7 @@ func (api *API) resolveChatGitHubAccessToken( continue } providerIDs = append(providerIDs, config.ID) + ghConfigs[config.ID] = config } seen := map[string]struct{}{} @@ -1471,6 +1476,24 @@ func (api *API) resolveChatGitHubAccessToken( continue } + // Refresh the token if there is a matching config, mirroring + // the same code path used by provisionerdserver when handing + // tokens to provisioners. + if cfg, ok := ghConfigs[providerID]; ok { + refreshed, refreshErr := cfg.RefreshToken(ctx, api.Database, link) + if refreshErr != nil { + api.Logger.Debug(ctx, "failed to refresh external auth token for chat diff", + slog.F("provider_id", providerID), + slog.F("user_id", userID), + slog.Error(refreshErr), + ) + // Fall through — the existing token may still work + // (e.g. GitHub tokens with no expiry). + } else { + link = refreshed + } + } + token := strings.TrimSpace(link.OAuthAccessToken) if token != "" { return token