From 1d0de9be51acb1642bd8f40f9f66cc066072e1ee Mon Sep 17 00:00:00 2001 From: Greg Katz Date: Fri, 30 Jan 2026 13:47:10 -0800 Subject: [PATCH 1/2] Fix OAuth token refresh context cancellation The TokenSource created in processToken() was using the OAuth flow's startup context, which gets cancelled when the callback server shuts down. When the access token expires (~1 hour later) and refresh is attempted, the HTTP request fails with "context canceled". This triggers a full re-authentication flow with new DCR client registration, requiring user interaction in the browser. The fix uses context.Background() for the TokenSource, since it is long-lived and must remain valid for token refresh operations long after the OAuth flow completes. Closes gh-3538 Signed-off-by: Greg Katz --- pkg/auth/oauth/flow.go | 9 ++++-- pkg/auth/oauth/flow_test.go | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/pkg/auth/oauth/flow.go b/pkg/auth/oauth/flow.go index 111c0e2bbb..c33563f844 100644 --- a/pkg/auth/oauth/flow.go +++ b/pkg/auth/oauth/flow.go @@ -457,8 +457,13 @@ func (f *Flow) processToken(ctx context.Context, token *oauth2.Token) *TokenResu Expiry: token.Expiry, } - // Create a base token source using the original token with the provided context - base := f.oauth2Config.TokenSource(ctx, token) + // Create a base token source using the original token with a background context. + // We use context.Background() instead of the passed ctx because the TokenSource + // is long-lived and will be used for token refresh operations long after the + // initial OAuth flow completes. Using the original ctx would cause "context canceled" + // errors when attempting to refresh tokens, as that context gets cancelled when + // the OAuth callback server shuts down. + base := f.oauth2Config.TokenSource(context.Background(), token) // ReuseTokenSource ensures that refresh happens only when needed f.tokenSource = oauth2.ReuseTokenSource(token, base) diff --git a/pkg/auth/oauth/flow_test.go b/pkg/auth/oauth/flow_test.go index cfd59a7f55..2af8e80f69 100644 --- a/pkg/auth/oauth/flow_test.go +++ b/pkg/auth/oauth/flow_test.go @@ -932,3 +932,65 @@ func TestExtractJWTClaims_ErrorCases(t *testing.T) { }) } } + +func TestTokenRefreshAfterContextCancellation(t *testing.T) { + t.Parallel() + + // Create a mock token server that tracks refresh attempts + refreshCalled := false + tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + require.NoError(t, err) + + if r.Form.Get("grant_type") == "refresh_token" { + refreshCalled = true + } + + response := map[string]interface{}{ + "access_token": "new-access-token", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "new-refresh-token", + } + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(response) + require.NoError(t, err) + })) + defer tokenServer.Close() + + config := &Config{ + ClientID: "test-client", + AuthURL: "https://example.com/auth", + TokenURL: tokenServer.URL, + } + + flow, err := NewFlow(config) + require.NoError(t, err) + + // Create a context that we will cancel (simulating OAuth flow completion) + ctx, cancel := context.WithCancel(context.Background()) + + // Process token with the cancellable context. + // Use an already-expired token to force refresh on next Token() call. + token := &oauth2.Token{ + AccessToken: "original-access-token", + RefreshToken: "test-refresh-token", + TokenType: "Bearer", + Expiry: time.Now().Add(-time.Hour), // Already expired + } + + _ = flow.processToken(ctx, token) + + // Cancel the context (simulates OAuth callback server shutdown) + cancel() + + // Now attempt to get a token - this should trigger refresh. + // Before the fix: fails with "context canceled" because processToken + // stored a TokenSource using the now-cancelled ctx. + // After the fix: succeeds because processToken uses context.Background(). + newToken, err := flow.tokenSource.Token() + + require.NoError(t, err, "token refresh should succeed even after context cancellation") + assert.True(t, refreshCalled, "refresh endpoint should have been called") + assert.Equal(t, "new-access-token", newToken.AccessToken) +} From 23f8b6109d9a4af1109083d889d2796941e4e2f1 Mon Sep 17 00:00:00 2001 From: Greg Katz Date: Fri, 30 Jan 2026 17:38:15 -0800 Subject: [PATCH 2/2] Fix unused ctx parameter lint error --- pkg/auth/oauth/flow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/auth/oauth/flow.go b/pkg/auth/oauth/flow.go index c33563f844..54f5a55de6 100644 --- a/pkg/auth/oauth/flow.go +++ b/pkg/auth/oauth/flow.go @@ -449,7 +449,7 @@ func (*Flow) writeErrorPage(w http.ResponseWriter, err error) { } // processToken processes the received token and extracts claims -func (f *Flow) processToken(ctx context.Context, token *oauth2.Token) *TokenResult { +func (f *Flow) processToken(_ context.Context, token *oauth2.Token) *TokenResult { result := &TokenResult{ AccessToken: token.AccessToken, RefreshToken: token.RefreshToken,