From 556737e0fd555bb6612e0f5e3fb2033ae0be496c Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Sun, 17 May 2026 16:40:15 +0530 Subject: [PATCH] backend: OIDC: Return error on state generation failure --- backend/cmd/headlamp.go | 19 +++++++++++-------- backend/cmd/headlamp_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 6e7c7f70863..31767ea388b 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -914,14 +914,7 @@ func createHeadlampHandler(ctx context.Context, config *HeadlampConfig) http.Han } // state should be unique per request, cryptographically secure random, url safe - state, err := func() (string, error) { - b := make([]byte, 32) - if _, err := rand.Read(b); err != nil { - return "", fmt.Errorf("generating OIDC state: %w", err) - } - - return base64.RawURLEncoding.EncodeToString(b), nil - }() + state, err := generateOidcState(rand.Reader) if err != nil { logger.Log(logger.LevelError, map[string]string{"cluster": cluster}, err, "failed to generate OIDC state") http.Error(w, "failed to generate OIDC state", http.StatusInternalServerError) @@ -1365,6 +1358,16 @@ func isLoopbackAddr(addr string) bool { return ip != nil && ip.IsLoopback() } +func generateOidcState(reader io.Reader) (string, error) { + b := make([]byte, 32) + + if _, err := io.ReadFull(reader, b); err != nil { + return "", err + } + + return base64.RawURLEncoding.EncodeToString(b), nil +} + // allowedHosts returns the set of normalized host values that are considered // valid for the given listen address and port. All entries are lowercased and // host:port pairs are built with net.JoinHostPort so that IPv6 literals are diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 6ce3a6c3beb..6f1baef401e 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -24,6 +24,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/json" + "errors" "fmt" "io" "net" @@ -1830,6 +1831,41 @@ func TestOidcCallbackEmptyStateDoesNotLogStaleError(t *testing.T) { "missing-state log must not carry a stale error from the outer createHeadlampHandler scope") } +func TestGenerateOidcState(t *testing.T) { + t.Run("success", func(t *testing.T) { + stateBytes := make([]byte, 32) + for i := range stateBytes { + stateBytes[i] = byte(i) + } + + state, err := generateOidcState(bytes.NewReader(stateBytes)) + require.NoError(t, err) + assert.Equal(t, "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8", state) + }) + + t.Run("failure", func(t *testing.T) { + state, err := generateOidcState(errReader{err: errors.New("rand failure")}) + + require.Error(t, err) + assert.Empty(t, state) + }) + + t.Run("short read", func(t *testing.T) { + state, err := generateOidcState(bytes.NewReader(make([]byte, 31))) + + require.ErrorIs(t, err, io.ErrUnexpectedEOF) + assert.Empty(t, state) + }) +} + +type errReader struct { + err error +} + +func (r errReader) Read([]byte) (int, error) { + return 0, r.err +} + func TestOIDCTokenRefreshMiddleware(t *testing.T) { kubeConfigStore := kubeconfig.NewContextStore() config := &HeadlampConfig{