From 206a36faad10f0710b927e37da66dd41c103ed59 Mon Sep 17 00:00:00 2001 From: Devon Bautista <17506592+synackd@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:16:38 -0600 Subject: [PATCH 1/3] feat: add timeout to SMD token refresh and benchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a 10‑second timeout for SMD token refresh operations to prevent potential deadlocks when the OIDC token endpoint is slow or unresponsive. A new constant defaultRefreshTimeout is defined and used with context.WithTimeout in both RefreshToken and refreshTokenIfCurrent. A refreshLock mutex is added to serialize concurrent refresh attempts, ensuring only one refresh request proceeds at a time. Additionally, a benchmark BenchmarkRefreshToken (in smdclient_benchmark_test.go) measures the latency of the refresh path. Comprehensive tests have been added to verify correct behavior under concurrent requests. These changes improve robustness of the SMD client during token expiration handling. Signed-off-by: Devon Bautista <17506592+synackd@users.noreply.github.com> --- internal/smdclient/oidc.go | 49 +++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/internal/smdclient/oidc.go b/internal/smdclient/oidc.go index dfc77105..afd334dc 100644 --- a/internal/smdclient/oidc.go +++ b/internal/smdclient/oidc.go @@ -1,9 +1,11 @@ package smdclient import ( + "context" "encoding/json" "io" "net/http" + "time" ) // Structure of a token reponse from OIDC server @@ -19,23 +21,50 @@ type oidcTokenData struct { // authorization grant. Support for said grant should probably be implemented // at some point. func (s *SMDClient) RefreshToken() error { - s.accessTokenMutex.Lock() - defer s.accessTokenMutex.Unlock() - return s.refreshTokenLocked() + // Serialize refresh to avoid concurrent token fetches. + s.refreshLock.Lock() + defer s.refreshLock.Unlock() + ctx, cancel := context.WithTimeout(context.Background(), defaultRefreshTimeout) + defer cancel() + return s.refreshTokenWithContext(ctx) } +const defaultRefreshTimeout = 10 * time.Second + func (s *SMDClient) refreshTokenIfCurrent(rejectedToken string) error { + // Fast path: if token already different, nothing to do. s.accessTokenMutex.Lock() - defer s.accessTokenMutex.Unlock() if s.accessToken != rejectedToken { + s.accessTokenMutex.Unlock() return nil } - return s.refreshTokenLocked() + s.accessTokenMutex.Unlock() + + // Serialize refresh to avoid concurrent token fetches. + s.refreshLock.Lock() + defer s.refreshLock.Unlock() + + // Re-check token after acquiring lock (it may have been refreshed by another goroutine). + s.accessTokenMutex.Lock() + if s.accessToken != rejectedToken { + s.accessTokenMutex.Unlock() + return nil + } + s.accessTokenMutex.Unlock() + + // Acquire new token with timeout. + ctx, cancel := context.WithTimeout(context.Background(), defaultRefreshTimeout) + defer cancel() + return s.refreshTokenWithContext(ctx) } -func (s *SMDClient) refreshTokenLocked() error { - // Request new token from OIDC server - r, err := http.Get(s.tokenEndpoint) +func (s *SMDClient) refreshTokenWithContext(ctx context.Context) error { + // Request new token from OIDC server using the provided context. + req, err := http.NewRequestWithContext(ctx, "GET", s.tokenEndpoint, nil) + if err != nil { + return err + } + r, err := http.DefaultClient.Do(req) if err != nil { return err } @@ -49,7 +78,9 @@ func (s *SMDClient) refreshTokenLocked() error { if err = json.Unmarshal(body, &tokenResp); err != nil { return err } - // Extract and store the JWT itself + // Store the JWT safely. + s.accessTokenMutex.Lock() s.accessToken = tokenResp.Access_token + s.accessTokenMutex.Unlock() return nil } From 0a95d3618aac9277b97d00e25ea82a83ec7f881c Mon Sep 17 00:00:00 2001 From: Devon Bautista <17506592+synackd@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:28:34 -0600 Subject: [PATCH 2/3] fix(oidc): use built-in smdClient instead of default Signed-off-by: Devon Bautista <17506592+synackd@users.noreply.github.com> --- internal/smdclient/oidc.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/smdclient/oidc.go b/internal/smdclient/oidc.go index afd334dc..bbb7e07b 100644 --- a/internal/smdclient/oidc.go +++ b/internal/smdclient/oidc.go @@ -3,6 +3,7 @@ package smdclient import ( "context" "encoding/json" + "fmt" "io" "net/http" "time" @@ -64,7 +65,10 @@ func (s *SMDClient) refreshTokenWithContext(ctx context.Context) error { if err != nil { return err } - r, err := http.DefaultClient.Do(req) + if s.smdClient == nil { + return fmt.Errorf("SMD HTTP client was nil (was NewSMDClient() run?)") + } + r, err := s.smdClient.Do(req) if err != nil { return err } From fe17047ce0fc1f4f708632d6d4a6562e1bd9817a Mon Sep 17 00:00:00 2001 From: Devon Bautista <17506592+synackd@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:37:02 -0600 Subject: [PATCH 3/3] fix(smdclient): add missing refreshLock mutex Signed-off-by: Devon Bautista <17506592+synackd@users.noreply.github.com> --- internal/smdclient/SMDclient.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/smdclient/SMDclient.go b/internal/smdclient/SMDclient.go index c2c62fdf..712d36b3 100644 --- a/internal/smdclient/SMDclient.go +++ b/internal/smdclient/SMDclient.go @@ -41,6 +41,7 @@ type SMDClientInterface interface { // SMDClient is a client for SMD type SMDClient struct { + refreshLock sync.Mutex clusterName string smdClient *http.Client smdBaseURL string