From cbb23faba09a702d70759390c7038965a0226354 Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Wed, 10 Jun 2026 21:02:52 +0000 Subject: [PATCH 1/3] fix(coderd/externalauth): disable refresh retries via negative RefreshRetryTimeout TestRefreshToken/RefreshRetries flaked on Windows because it disabled transient-failure retries with RefreshRetryTimeout = time.Nanosecond. A near-zero timeout is not deterministic: on coarse-clock platforms the 1ns deadline may not register as expired until after the first refresh attempt completes, and retry.Wait's first delay is zero, so an extra IDP refresh attempt slips through and the attempt-count assertion fails (refreshCount = totalRefreshes + 1). Make a negative RefreshRetryTimeout disable transient-failure retries explicitly so exactly one refresh attempt is made, and use -1 in the test instead of time.Nanosecond. Closes https://github.com/coder/internal/issues/1550 (PLAT-293) --- coderd/externalauth/externalauth.go | 16 +++++++++++++--- coderd/externalauth/externalauth_test.go | 16 ++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go index 25777516c8a..07476c0df00 100644 --- a/coderd/externalauth/externalauth.go +++ b/coderd/externalauth/externalauth.go @@ -139,7 +139,8 @@ type Config struct { RefreshRetryMaxBackoff time.Duration // RefreshRetryTimeout overrides the total budget for retrying a transient // refresh failure across all attempts. A zero value applies - // defaultRefreshRetryTimeout. + // defaultRefreshRetryTimeout. A negative value disables transient-failure + // retries entirely, so exactly one refresh attempt is made. RefreshRetryTimeout time.Duration } @@ -400,6 +401,15 @@ func (c *Config) refreshTokenWithRetry(ctx context.Context, existingToken *oauth return c.TokenSource(ctx, existingToken).Token() } + // A negative RefreshRetryTimeout disables retries: exactly one refresh + // attempt is made. A near-zero timeout cannot deterministically prevent + // a retry because on platforms with coarse clocks (notably Windows) the + // deadline may not register as expired until after the first attempt + // completes, allowing an extra zero-delay retry. + if c.RefreshRetryTimeout < 0 { + return c.TokenSource(ctx, existingToken).Token() + } + initial := c.RefreshRetryInitialBackoff if initial <= 0 { initial = defaultRefreshRetryInitialBackoff @@ -409,7 +419,7 @@ func (c *Config) refreshTokenWithRetry(ctx context.Context, existingToken *oauth maximum = defaultRefreshRetryMaxBackoff } total := c.RefreshRetryTimeout - if total <= 0 { + if total == 0 { total = defaultRefreshRetryTimeout } @@ -430,7 +440,7 @@ func (c *Config) refreshTokenWithRetry(ctx context.Context, existingToken *oauth // retry.Wait selects between time.After(delay) and ctx.Done(); when // delay is zero and the context is already canceled the two cases // race nondeterministically, which would cause an unwanted extra - // refresh attempt with a near-zero budget (notably in tests). + // refresh attempt with a near-zero budget. if retryCtx.Err() != nil { return token, err } diff --git a/coderd/externalauth/externalauth_test.go b/coderd/externalauth/externalauth_test.go index 526c36c44c5..d4ebdc9a6f9 100644 --- a/coderd/externalauth/externalauth_test.go +++ b/coderd/externalauth/externalauth_test.go @@ -156,9 +156,10 @@ func TestRefreshToken(t *testing.T) { // refresh attempts should ever happen. An invalid refresh token does // not magically become valid at some point in the future. // - // Internal retries are disabled in this subtest via RefreshRetryTimeout - // so each RefreshToken call results in exactly one IDP refresh attempt. - // The RefreshTokenWithBackoff subtest covers the retry-with-backoff path. + // Internal retries are disabled in this subtest via a negative + // RefreshRetryTimeout so each RefreshToken call results in exactly one + // IDP refresh attempt. The RefreshTokenWithBackoff subtest covers the + // retry-with-backoff path. t.Run("RefreshRetries", func(t *testing.T) { t.Parallel() @@ -182,9 +183,12 @@ func TestRefreshToken(t *testing.T) { }), }, ExternalAuthOpt: func(cfg *externalauth.Config) { - // Disable transient-error retries so the assertion below - // (1 IDP call per RefreshToken) holds. - cfg.RefreshRetryTimeout = time.Nanosecond + // A negative timeout disables transient-error retries so the + // assertion below (1 IDP call per RefreshToken) holds. A tiny + // positive timeout is not deterministic: on coarse-clock + // platforms (Windows) the deadline may not register as expired + // after the first attempt, letting a zero-delay retry through. + cfg.RefreshRetryTimeout = -1 }, }) From 0cbd71ab86a9684f7fc9b8ab73f65610aedc26e3 Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Wed, 10 Jun 2026 22:33:49 +0000 Subject: [PATCH 2/3] chore(coderd/externalauth): condense refresh retry comments --- coderd/externalauth/externalauth.go | 13 +++++-------- coderd/externalauth/externalauth_test.go | 8 +++----- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go index 07476c0df00..9a4e2980ed6 100644 --- a/coderd/externalauth/externalauth.go +++ b/coderd/externalauth/externalauth.go @@ -390,9 +390,9 @@ validate: // refreshTokenWithRetry exchanges the refresh token for a new access token, // retrying with exponential backoff on transient failures. Permanent -// failures (as classified by isFailedRefresh) and the no-op case where no -// refresh token is set bypass the retry loop so a doomed refresh is not -// repeatedly attempted. +// failures (as classified by isFailedRefresh), the no-op case where no +// refresh token is set, and a negative RefreshRetryTimeout all bypass the +// retry loop so a doomed or unwanted refresh is not repeatedly attempted. func (c *Config) refreshTokenWithRetry(ctx context.Context, existingToken *oauth2.Token) (*oauth2.Token, error) { // Without a refresh token the oauth2 library short-circuits with // "token expired and refresh token is not set". No retry can recover @@ -401,11 +401,8 @@ func (c *Config) refreshTokenWithRetry(ctx context.Context, existingToken *oauth return c.TokenSource(ctx, existingToken).Token() } - // A negative RefreshRetryTimeout disables retries: exactly one refresh - // attempt is made. A near-zero timeout cannot deterministically prevent - // a retry because on platforms with coarse clocks (notably Windows) the - // deadline may not register as expired until after the first attempt - // completes, allowing an extra zero-delay retry. + // A near-zero positive timeout is nondeterministic on coarse-clock + // platforms, so a negative value explicitly disables retries. if c.RefreshRetryTimeout < 0 { return c.TokenSource(ctx, existingToken).Token() } diff --git a/coderd/externalauth/externalauth_test.go b/coderd/externalauth/externalauth_test.go index d4ebdc9a6f9..4221e733090 100644 --- a/coderd/externalauth/externalauth_test.go +++ b/coderd/externalauth/externalauth_test.go @@ -183,11 +183,9 @@ func TestRefreshToken(t *testing.T) { }), }, ExternalAuthOpt: func(cfg *externalauth.Config) { - // A negative timeout disables transient-error retries so the - // assertion below (1 IDP call per RefreshToken) holds. A tiny - // positive timeout is not deterministic: on coarse-clock - // platforms (Windows) the deadline may not register as expired - // after the first attempt, letting a zero-delay retry through. + // Negative timeout disables retries (1 IDP call per RefreshToken). + // A tiny positive timeout is unreliable on coarse-clock platforms + // (Windows). cfg.RefreshRetryTimeout = -1 }, }) From f6e6771963ea2212ae6426b3fcf7cdb36f182f42 Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Thu, 11 Jun 2026 15:22:29 +0000 Subject: [PATCH 3/3] chore(coderd/externalauth): decouple retry disable comment from flake --- coderd/externalauth/externalauth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go index 9a4e2980ed6..9b11b7f3ed7 100644 --- a/coderd/externalauth/externalauth.go +++ b/coderd/externalauth/externalauth.go @@ -401,8 +401,8 @@ func (c *Config) refreshTokenWithRetry(ctx context.Context, existingToken *oauth return c.TokenSource(ctx, existingToken).Token() } - // A near-zero positive timeout is nondeterministic on coarse-clock - // platforms, so a negative value explicitly disables retries. + // A negative RefreshRetryTimeout disables retries entirely, so make a + // single attempt and return. if c.RefreshRetryTimeout < 0 { return c.TokenSource(ctx, existingToken).Token() }