From a3d995829e4bf46afc5183ed6af07a781db9b823 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:21:48 -0700 Subject: [PATCH 1/9] fix(template): change UpdateTemplateArg.KeyUsage to *int to match Command API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Command's TemplateUpdateRequest.KeyUsage and TemplateRetrievalResponse.KeyUsage are both {"type":"integer","format":"int32"} per the v25.5 swagger — an int32 bitmask (e.g. 160 = digitalSignature|keyEncipherment). UpdateTemplateArg.KeyUsage was typed *bool, which serializes as a JSON boolean and produces a live HTTP 400 from Command ("Unexpected character encountered while parsing value: t. Path 'KeyUsage'"), making the field unusable as-is. GetTemplateResponse.KeyUsage was already int, so this also fixes the type mismatch between the get and update models for the same field. Also fixes the identical defect in v2/api/template_models.go for consistency; v2 is tagged/released independently and is not part of this v3.6.0 change. Adds TestUpdateTemplateArg_KeyUsage_SerializesAsInt to v3/api/template_test.go, which fails to compile against the pre-fix *bool field and asserts the wire payload is a JSON number. --- v2/api/template_models.go | 8 +++++- v3/api/template_models.go | 8 +++++- v3/api/template_test.go | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/v2/api/template_models.go b/v2/api/template_models.go index 526b8c4..70eefd7 100644 --- a/v2/api/template_models.go +++ b/v2/api/template_models.go @@ -81,7 +81,13 @@ type UpdateTemplateArg struct { AllowedRequesters *[]string `json:"AllowedRequesters,omitempty"` RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` RequiresApproval *bool `json:"RequiresApproval,omitempty"` - KeyUsage *bool `json:"KeyUsage,omitempty"` + // KeyUsage is an int32 bitmask on Command's wire format (e.g. 160 = + // digitalSignature|keyEncipherment), matching GetTemplateResponse.KeyUsage and + // Command's TemplateUpdateRequest/TemplateRetrievalResponse swagger schema + // (both typed "integer"/"int32"). A *bool here previously produced a live + // HTTP 400 ("Unexpected character encountered while parsing value: t. Path + // 'KeyUsage'") since Command rejects a JSON boolean for an integer field. + KeyUsage *int `json:"KeyUsage,omitempty"` } type UpdateTemplateResponse struct{ GetTemplateResponse } diff --git a/v3/api/template_models.go b/v3/api/template_models.go index 127683b..39d905b 100644 --- a/v3/api/template_models.go +++ b/v3/api/template_models.go @@ -119,7 +119,13 @@ type UpdateTemplateArg struct { AllowedRequesters *[]string `json:"AllowedRequesters,omitempty"` RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` RequiresApproval *bool `json:"RequiresApproval,omitempty"` - KeyUsage *bool `json:"KeyUsage,omitempty"` + // KeyUsage is an int32 bitmask on Command's wire format (e.g. 160 = + // digitalSignature|keyEncipherment), matching GetTemplateResponse.KeyUsage and + // Command's TemplateUpdateRequest/TemplateRetrievalResponse swagger schema + // (both typed "integer"/"int32"). A *bool here previously produced a live + // HTTP 400 ("Unexpected character encountered while parsing value: t. Path + // 'KeyUsage'") since Command rejects a JSON boolean for an integer field. + KeyUsage *int `json:"KeyUsage,omitempty"` // TemplatePolicy must be round-tripped from the corresponding GetTemplateResponse // on every update; see the field comment on GetTemplateResponse.TemplatePolicy. TemplatePolicy *TemplatePolicy `json:"TemplatePolicy,omitempty"` diff --git a/v3/api/template_test.go b/v3/api/template_test.go index c18c629..ee27197 100644 --- a/v3/api/template_test.go +++ b/v3/api/template_test.go @@ -244,3 +244,62 @@ func TestUpdateTemplateArg_TemplatePolicy_Roundtrip(t *testing.T) { t.Fatalf("TemplatePolicy.PrimaryKeyAlgorithms on the wire = %v, want 2 entries", policy["PrimaryKeyAlgorithms"]) } } + +// TestUpdateTemplateArg_KeyUsage_SerializesAsInt verifies that UpdateTemplateArg.KeyUsage +// serializes onto the wire as a JSON number, matching Command's TemplateUpdateRequest +// swagger schema ({"type":"integer","format":"int32"}) confirmed against a live v25.5 +// instance. Before the fix, KeyUsage was typed *bool, which serialized as a JSON boolean +// and produced a live HTTP 400 from Command ("Unexpected character encountered while +// parsing value: t. Path 'KeyUsage'"). This also verifies the value returned by +// GetTemplateResponse.KeyUsage (an int) can be assigned directly to +// UpdateTemplateArg.KeyUsage without a type conversion, since both now agree on int. +func TestUpdateTemplateArg_KeyUsage_SerializesAsInt(t *testing.T) { + var receivedBody []byte + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + receivedBody, err = io.ReadAll(r.Body) + if err != nil { + t.Fatalf("failed to read request body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(receivedBody) + })) + defer srv.Close() + + c := newTestClient(srv) + + // Simulate a real read-modify-write: KeyUsage comes straight off a + // GetTemplateResponse (int) with no bool<->int conversion required. + fetched := GetTemplateResponse{Id: 4, KeyUsage: 160} // digitalSignature|keyEncipherment + keyUsage := fetched.KeyUsage + + arg := &UpdateTemplateArg{ + Id: 4, + KeyUsage: &keyUsage, + } + + if _, err := c.UpdateTemplate(arg); err != nil { + t.Fatalf("UpdateTemplate() error: %v", err) + } + + var onWire map[string]interface{} + if err := json.Unmarshal(receivedBody, &onWire); err != nil { + t.Fatalf("failed to decode request body sent to server: %v", err) + } + + rawKeyUsage, ok := onWire["KeyUsage"] + if !ok { + t.Fatalf("request body sent to server has no KeyUsage field; got keys: %v", onWire) + } + switch v := rawKeyUsage.(type) { + case float64: + if v != 160 { + t.Errorf("KeyUsage on the wire = %v, want 160", v) + } + case bool: + t.Fatalf("KeyUsage on the wire is a JSON boolean (%v); Command's API expects an int32 bitmask and returns HTTP 400 for a boolean payload", v) + default: + t.Fatalf("KeyUsage on the wire has unexpected type %T (value %v), want a JSON number", v, v) + } +} From e46009a31a94c27ea6e9392739106d63f566c4ca Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:38:21 -0700 Subject: [PATCH 2/9] fix(client): plumb Server.ClientTimeout into rebuilt auth config NewKeyfactorClient rebuilds a fresh CommandAuthConfig from the caller's *auth_providers.Server instead of reusing the one that produced it, but never carried over ClientTimeout. Every consumer -- including the Terraform provider's request_timeout setting -- ended up authenticating and issuing requests with DefaultClientTimeout (60s) regardless of what was configured, causing "net/http: timeout awaiting response headers" on long-running calls like PFX enrollment. Set HttpClientTimeout: cfg.ClientTimeout in the baseConfig literal so it flows into BuildTransport()/SetClient() for both the basic and oauth auth paths. Depends on github.com/Keyfactor/keyfactor-auth-client-go#51 being fixed upstream (Server.ClientTimeout field). go.mod is bumped to the not-yet-tagged v1.6.0-rc.1 and pinned locally via a `replace` directive at /tmp/kf-worktrees/kfc-auth for testing; once that tag is cut, drop the replace and re-run `go mod tidy`. --- v3/api/client.go | 11 ++-- v3/api/client_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++ v3/go.mod | 8 ++- v3/go.sum | 4 -- 4 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 v3/api/client_test.go diff --git a/v3/api/client.go b/v3/api/client.go index add6a21..8be9d2c 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -142,11 +142,12 @@ func NewKeyfactorClient(cfg *auth_providers.Server, ctx *context.Context) (*Clie clientAuthType := cfg.GetAuthType() baseConfig := auth_providers.CommandAuthConfig{ - CommandHostName: cfg.Host, - CommandPort: cfg.Port, - CommandAPIPath: cfg.APIPath, - CommandCACert: cfg.CACertPath, - SkipVerify: cfg.SkipTLSVerify, + CommandHostName: cfg.Host, + CommandPort: cfg.Port, + CommandAPIPath: cfg.APIPath, + CommandCACert: cfg.CACertPath, + SkipVerify: cfg.SkipTLSVerify, + HttpClientTimeout: cfg.ClientTimeout, } if clientAuthType == "basic" { diff --git a/v3/api/client_test.go b/v3/api/client_test.go new file mode 100644 index 0000000..212ec35 --- /dev/null +++ b/v3/api/client_test.go @@ -0,0 +1,125 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +// newFakeCommandServer stands in for a Keyfactor Command instance for +// CommandAuthConfigBasic.Authenticate(), which performs a real GET against +// {host}/{apiPath}/Status/Endpoints as part of authentication. It always +// returns 200 with a valid JSON string array, regardless of credentials. +func newFakeCommandServer(t *testing.T) *httptest.Server { + t.Helper() + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server +} + +// TestNewKeyfactorClient_PlumbsClientTimeout is a regression test proving that +// a Server.ClientTimeout value survives NewKeyfactorClient's rebuild of the +// CommandAuthConfig. Before this fix, baseConfig never set HttpClientTimeout, +// so the rebuilt auth config (and everything derived from it, including +// BuildTransport's ResponseHeaderTimeout) silently fell back to +// DefaultClientTimeout (60s) regardless of what the caller configured, +// producing "net/http: timeout awaiting response headers" on long-running +// calls such as PFX enrollment. +func TestNewKeyfactorClient_PlumbsClientTimeout(t *testing.T) { + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + cfg := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 300, + } + + ctx := context.Background() + client, err := NewKeyfactorClient(cfg, &ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + basicCfg, ok := client.AuthClient.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthClient to be *auth_providers.CommandAuthConfigBasic, got %T", client.AuthClient) + } + + if basicCfg.HttpClientTimeout != 300 { + t.Fatalf("expected HttpClientTimeout to be 300, got %d", basicCfg.HttpClientTimeout) + } + + transport, tErr := basicCfg.CommandAuthConfig.BuildTransport() + if tErr != nil { + t.Fatalf("expected no error building transport, got %v", tErr) + } + + expected := 300 * time.Second + if transport.ResponseHeaderTimeout != expected { + t.Fatalf("expected ResponseHeaderTimeout to be %v, got %v", expected, transport.ResponseHeaderTimeout) + } +} + +// TestNewKeyfactorClient_DefaultClientTimeout confirms the zero-value +// (unset) case still falls back to the library default rather than 0s, +// preserving pre-fix behavior for callers who don't set ClientTimeout. +func TestNewKeyfactorClient_DefaultClientTimeout(t *testing.T) { + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + cfg := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + } + + ctx := context.Background() + client, err := NewKeyfactorClient(cfg, &ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + basicCfg, ok := client.AuthClient.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthClient to be *auth_providers.CommandAuthConfigBasic, got %T", client.AuthClient) + } + + if basicCfg.HttpClientTimeout != auth_providers.DefaultClientTimeout { + t.Fatalf("expected HttpClientTimeout to fall back to default %d, got %d", auth_providers.DefaultClientTimeout, basicCfg.HttpClientTimeout) + } +} diff --git a/v3/go.mod b/v3/go.mod index f4eb047..1e61246 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,11 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.5.0 + // TODO(fix/server-client-timeout): bump to v1.6.0-rc.1 once that tag is cut + // upstream (fixes Server.ClientTimeout plumbing, see + // https://github.com/Keyfactor/keyfactor-auth-client-go/issues/51), then + // remove the local `replace` below and re-run `go mod tidy`. + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.1 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 @@ -55,3 +59,5 @@ require ( golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/Keyfactor/keyfactor-auth-client-go => /tmp/kf-worktrees/kfc-auth diff --git a/v3/go.sum b/v3/go.sum index 451aad8..6b9ffa5 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.5.0 h1:sq7SGkJeTtDspFSuX2oJxTmFiiFfaQ68B4JP7jryl94= -github.com/Keyfactor/keyfactor-auth-client-go v1.5.0/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -75,8 +73,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= -github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/spbsoluble/go-pkcs12 v0.4.0 h1:3HOVPZ8pvYqhAyz/NJzT9YODQJ3HbZvB9/CMVmvGaUM= github.com/spbsoluble/go-pkcs12 v0.4.0/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 4ef0c0a530d5acc0bd4ee00f3551c51f9d549689 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:08:32 -0700 Subject: [PATCH 3/9] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.2 Removes the local replace directive and TODO now that the ClientTimeout fix is published, and validates against the published dependency. --- v3/go.mod | 8 +------- v3/go.sum | 2 ++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 1e61246..e061bc3 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,11 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - // TODO(fix/server-client-timeout): bump to v1.6.0-rc.1 once that tag is cut - // upstream (fixes Server.ClientTimeout plumbing, see - // https://github.com/Keyfactor/keyfactor-auth-client-go/issues/51), then - // remove the local `replace` below and re-run `go mod tidy`. - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.1 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 @@ -59,5 +55,3 @@ require ( golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) - -replace github.com/Keyfactor/keyfactor-auth-client-go => /tmp/kf-worktrees/kfc-auth diff --git a/v3/go.sum b/v3/go.sum index 6b9ffa5..670986e 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,6 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 h1:wp7LBuNSpHZYPlzEuipNeuWwwBow8lgLj8lD2gMivhM= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 40a77564924281fc5c40e5f7d7ecbb97e5c26927 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:55:21 -0700 Subject: [PATCH 4/9] fix(client): cache and reuse http client across requests Client.sendRequest called AuthConfig.GetHttpClient() on every single request. Both CommandConfigOauth and CommandAuthConfigBasic in keyfactor-auth-client-go build a brand new http.Transport (and therefore a brand new, empty connection pool) on each call, and that transport's IdleConnTimeout is derived from the configured HttpClientTimeout - so every API call opened its own never-reused connection whose socket lingered until IdleConnTimeout fired. This leak predates this branch at the fixed 60s default; plumbing a caller-configured ClientTimeout through (which can be arbitrarily large, e.g. 1800s for slow enrollments) widens the linger window proportionally, so cache the *http.Client on Client and reuse it across requests instead of rebuilding it per call. The OAuth token source is still consulted (and refreshed) on every RoundTrip independent of how many times the *http.Client is reused, and NewKeyfactorClientWithAuth (used by VCR/unit tests) still works by lazily populating the cache on first use. --- v3/api/client.go | 50 +++++++++++++++++-- v3/api/client_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/v3/api/client.go b/v3/api/client.go index 8be9d2c..b8c63d0 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -28,6 +28,7 @@ import ( "net/url" "path" "strings" + "sync" "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" @@ -69,6 +70,47 @@ var ( type Client struct { AuthClient AuthConfig LoggerType string + + // httpClient caches the *http.Client returned by AuthClient.GetHttpClient() + // so that sendRequest reuses a single underlying transport/connection pool + // across requests instead of asking AuthClient to build a brand new one on + // every call. Both CommandConfigOauth.GetHttpClient() and + // CommandAuthConfigBasic.GetHttpClient() (in keyfactor-auth-client-go) + // construct a fresh http.Transport per invocation, and that transport's + // IdleConnTimeout is derived from the configured HttpClientTimeout - so + // without this cache, every request opens its own connection pool whose + // sockets linger for up to HttpClientTimeout before being reclaimed. This + // was already true at the old fixed 60s default; plumbing a caller-supplied + // ClientTimeout (see NewKeyfactorClient) just widens the window, so caching + // here keeps that fix from amplifying a pre-existing resource leak. + httpClient *http.Client + httpClientMu sync.Mutex +} + +// getHttpClient returns the cached *http.Client if one has already been +// resolved for this Client, populating the cache on first use otherwise. +// This guarantees AuthClient.GetHttpClient() is invoked at most once per +// Client instance, so the transport (and its connection pool) is reused +// across requests. It is safe for concurrent use. +// +// Note this does not affect OAuth token refresh: the cached *http.Client's +// transport wraps an oauth2 TokenSource that is consulted (and refreshed as +// needed) on every RoundTrip, independent of how many times the *http.Client +// itself is reused. +func (c *Client) getHttpClient() (*http.Client, error) { + c.httpClientMu.Lock() + defer c.httpClientMu.Unlock() + + if c.httpClient != nil { + return c.httpClient, nil + } + + httpClient, err := c.AuthClient.GetHttpClient() + if err != nil { + return nil, err + } + c.httpClient = httpClient + return httpClient, nil } // TerraformLogger wraps the tflog logging to handle Go's log messages with log level mapping. @@ -161,11 +203,12 @@ func NewKeyfactorClient(cfg *auth_providers.Server, ctx *context.Context) (*Clie if aErr != nil { return nil, aErr } - _, cErr := basicCfg.GetHttpClient() + httpClient, cErr := basicCfg.GetHttpClient() if cErr != nil { return nil, cErr } client.AuthClient = &basicCfg + client.httpClient = httpClient return &client, nil } else if clientAuthType == "oauth" { oauthCfg := auth_providers.CommandConfigOauth{ @@ -181,11 +224,12 @@ func NewKeyfactorClient(cfg *auth_providers.Server, ctx *context.Context) (*Clie if aErr != nil { return nil, aErr } - _, cErr := oauthCfg.GetHttpClient() + httpClient, cErr := oauthCfg.GetHttpClient() if cErr != nil { return nil, cErr } client.AuthClient = &oauthCfg + client.httpClient = httpClient return &client, nil } else { return nil, fmt.Errorf("unsupported auth type or authentication cfg: '%s'", clientAuthType) @@ -343,7 +387,7 @@ func (c *Client) sendRequest(request *request) (*http.Response, error) { // Log the request logRequest(req) - httpClient, cErr := c.AuthClient.GetHttpClient() + httpClient, cErr := c.getHttpClient() if cErr != nil { return nil, cErr } diff --git a/v3/api/client_test.go b/v3/api/client_test.go index 212ec35..f7123b0 100644 --- a/v3/api/client_test.go +++ b/v3/api/client_test.go @@ -16,9 +16,13 @@ package api import ( "context" + "crypto/tls" + "io" + "net" "net/http" "net/http/httptest" "net/url" + "sync/atomic" "testing" "time" @@ -123,3 +127,107 @@ func TestNewKeyfactorClient_DefaultClientTimeout(t *testing.T) { t.Fatalf("expected HttpClientTimeout to fall back to default %d, got %d", auth_providers.DefaultClientTimeout, basicCfg.HttpClientTimeout) } } + +// perCallTransportAuthConfig is a minimal AuthConfig test double that mimics +// the real behavior of keyfactor-auth-client-go's CommandConfigOauth and +// CommandAuthConfigBasic GetHttpClient() implementations: every call builds a +// brand new *http.Transport (and therefore a brand new, empty connection +// pool) rather than reusing one. It exists to prove that Client caches the +// *http.Client it gets back rather than calling GetHttpClient() (and paying +// for a fresh transport/connection pool) on every request. +type perCallTransportAuthConfig struct { + server *httptest.Server + getClientCalls int32 +} + +func (a *perCallTransportAuthConfig) GetServerConfig() *auth_providers.Server { + return &auth_providers.Server{ + Host: a.server.URL, + APIPath: "KeyfactorAPI", + SkipTLSVerify: true, + } +} + +func (a *perCallTransportAuthConfig) GetHttpClient() (*http.Client, error) { + atomic.AddInt32(&a.getClientCalls, 1) + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + }, nil +} + +func (a *perCallTransportAuthConfig) Authenticate() error { return nil } + +func (a *perCallTransportAuthConfig) GetCommandVersion() string { return "25.1.0.0" } + +// TestClient_ReusesHttpClientAcrossRequests is a regression test for a +// resource leak: sendRequest used to call c.AuthClient.GetHttpClient() on +// every single request. Since the real AuthConfig implementations build a +// brand new http.Transport (and connection pool) per call, and that +// transport's IdleConnTimeout is derived from the configured +// HttpClientTimeout, every API call opened its own never-reused connection +// whose socket lingered until IdleConnTimeout fired - amplified by the fix +// that plumbs a caller-configured ClientTimeout (which can be arbitrarily +// large, e.g. 1800s) all the way through instead of the fixed 60s default. +// +// This test drives Client.sendRequest directly across multiple requests and +// asserts both that AuthConfig.GetHttpClient() is invoked at most once +// (proving the *http.Client is cached) and that the underlying TCP +// connection is reused rather than growing linearly with the request count. +func TestClient_ReusesHttpClientAcrossRequests(t *testing.T) { + var newConns int32 + srv := httptest.NewUnstartedServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[]`)) + }, + ), + ) + srv.Config.ConnState = func(_ net.Conn, state http.ConnState) { + if state == http.StateNew { + atomic.AddInt32(&newConns, 1) + } + } + srv.StartTLS() + t.Cleanup(srv.Close) + + auth := &perCallTransportAuthConfig{server: srv} + client := NewKeyfactorClientWithAuth(auth, nil) + + const requestCount = 10 + for i := 0; i < requestCount; i++ { + resp, err := client.sendRequest( + &request{ + Method: http.MethodGet, + Endpoint: "CertificateStoreContainers", + Headers: &apiHeaders{}, + }, + ) + if err != nil { + t.Fatalf("request %d failed: %v", i, err) + } + // Fully drain and close the body so the underlying transport is free + // to return the connection to its idle pool for reuse. + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } + + if calls := atomic.LoadInt32(&auth.getClientCalls); calls != 1 { + t.Fatalf( + "expected AuthClient.GetHttpClient to be called exactly once across %d requests (client should be cached), got %d calls", + requestCount, + calls, + ) + } + + if conns := atomic.LoadInt32(&newConns); conns > 2 { + t.Fatalf( + "expected the TCP connection to be reused across %d sequential requests (at most ~1-2 new connections), observed %d new connections", + requestCount, + conns, + ) + } +} From 9aa4970c67d4958bf21a708ad48a6d5eef4d6686 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:56:09 -0700 Subject: [PATCH 5/9] test(client): isolate ambient KEYFACTOR_* env vars in client tests TestNewKeyfactorClient_PlumbsClientTimeout and TestNewKeyfactorClient_DefaultClientTimeout build a Server config with fields intentionally left at their zero value to exercise ValidateAuthConfig's environment-variable fallback path. Because ValidateAuthConfig only falls back to KEYFACTOR_CLIENT_TIMEOUT/ KEYFACTOR_PORT/KEYFACTOR_CA_CERT when the struct field is unset, and unconditionally overwrites SkipVerify from KEYFACTOR_SKIP_VERIFY regardless of the struct field, ambient values for these variables (e.g. from a sourced lab env file) broke both tests: KEYFACTOR_CLIENT_TIMEOUT=120 flips the expected default from 60 to 120, and KEYFACTOR_SKIP_VERIFY=false clobbers SkipTLSVerify:true and rejects the tests' self-signed httptest TLS certificate. Add isolateKeyfactorEnv to unset the relevant variables for the duration of each test and restore their original values afterward. t.Setenv(key, "") does not work here since an empty value is still "present" to os.LookupEnv. --- v3/api/client_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/v3/api/client_test.go b/v3/api/client_test.go index f7123b0..0bbc14e 100644 --- a/v3/api/client_test.go +++ b/v3/api/client_test.go @@ -22,6 +22,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "sync/atomic" "testing" "time" @@ -44,6 +45,42 @@ func newFakeCommandServer(t *testing.T) *httptest.Server { return server } +// isolateKeyfactorEnv unsets ambient KEYFACTOR_* environment variables that +// CommandAuthConfig.ValidateAuthConfig() falls back to whenever the +// corresponding struct field is left at its zero value, restoring their +// original values (present-and-unset, or present-with-value) once the test +// completes. This makes tests that build a Server/CommandAuthConfig with an +// intentionally-zero field (e.g. ClientTimeout: 0 to exercise the "use the +// default" path, or SkipTLSVerify relying on a literal true) hermetic: +// without this, a developer or CI job with KEYFACTOR_CLIENT_TIMEOUT or +// KEYFACTOR_SKIP_VERIFY exported in their shell would get spurious failures +// or, worse, a silently-clobbered SkipVerify that rejects the test's +// self-signed httptest TLS cert. +// +// Note: t.Setenv(key, "") is NOT equivalent to unsetting - os.LookupEnv still +// reports the variable as present with an empty value, which is enough to +// take the "environment variable is set" branch in ValidateAuthConfig (e.g. +// strconv.Atoi("") fails silently and leaves HttpClientTimeout at 0 rather +// than falling through to DefaultClientTimeout). The variable must be +// actually removed from the environment. +func isolateKeyfactorEnv(t *testing.T, keys ...string) { + t.Helper() + for _, key := range keys { + key := key + originalValue, wasSet := os.LookupEnv(key) + if err := os.Unsetenv(key); err != nil { + t.Fatalf("failed to unset %s: %v", key, err) + } + t.Cleanup(func() { + if wasSet { + _ = os.Setenv(key, originalValue) + } else { + _ = os.Unsetenv(key) + } + }) + } +} + // TestNewKeyfactorClient_PlumbsClientTimeout is a regression test proving that // a Server.ClientTimeout value survives NewKeyfactorClient's rebuild of the // CommandAuthConfig. Before this fix, baseConfig never set HttpClientTimeout, @@ -53,6 +90,13 @@ func newFakeCommandServer(t *testing.T) *httptest.Server { // producing "net/http: timeout awaiting response headers" on long-running // calls such as PFX enrollment. func TestNewKeyfactorClient_PlumbsClientTimeout(t *testing.T) { + isolateKeyfactorEnv( + t, + auth_providers.EnvKeyfactorClientTimeout, + auth_providers.EnvKeyfactorSkipVerify, + auth_providers.EnvKeyfactorPort, + auth_providers.EnvKeyfactorCACert, + ) server := newFakeCommandServer(t) u, uErr := url.Parse(server.URL) if uErr != nil { @@ -98,6 +142,13 @@ func TestNewKeyfactorClient_PlumbsClientTimeout(t *testing.T) { // (unset) case still falls back to the library default rather than 0s, // preserving pre-fix behavior for callers who don't set ClientTimeout. func TestNewKeyfactorClient_DefaultClientTimeout(t *testing.T) { + isolateKeyfactorEnv( + t, + auth_providers.EnvKeyfactorClientTimeout, + auth_providers.EnvKeyfactorSkipVerify, + auth_providers.EnvKeyfactorPort, + auth_providers.EnvKeyfactorCACert, + ) server := newFakeCommandServer(t) u, uErr := url.Parse(server.URL) if uErr != nil { From 62dcb4958aa1b6f598c7cb1117babe437ba26dc9 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:19:40 -0700 Subject: [PATCH 6/9] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.3 Picks up the round-4 convergence fixes: ClientTimeout persistence gated across all three concrete auth types via delegation to the base type, a BOM-prefix bypass fix in nested-JSON secret redaction, MaxConnsPerHost widened to unbounded, and body redaction extended to cover JSON-in-string values. --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index e061bc3..b5760c4 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 diff --git a/v3/go.sum b/v3/go.sum index 670986e..93efff2 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 h1:wp7LBuNSpHZYPlzEuipNeuWwwBow8lgLj8lD2gMivhM= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 h1:1j0ZVOmay13SrpQkXrfaGBml8pEAsE7sDJzHHK6C2+Y= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From e157379e01e1fd81a09b924ef5257910e77c84da Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:19:51 -0700 Subject: [PATCH 7/9] test(client): verify concurrent requests are not capped at MaxConnsPerHost=10 Closes the loop on a finding this package's own http.Client-caching fix could not verify end-to-end: caching a single *http.Client turns the transport's MaxConnsPerHost into a permanent, unqueued-timeout concurrency ceiling for the process, since the cached client has no Timeout and requests carry no deadline. keyfactor-auth-client-go's fix (MaxConnsPerHost widened from a hardcoded 10 to unbounded) was only verified there by inspecting the constructed transport's field value. Add an end-to-end regression test that builds a real Client via NewKeyfactorClient, retrieves its cached *http.Client, and drives 25 concurrent requests through it against a real httptest server, asserting the server observes well more than 10 requests in flight at once. Confirmed this fails against v1.6.0-rc.2 (10 in-flight, ~620ms) and passes against v1.6.0-rc.3 (25 in-flight, ~225ms). --- v3/api/client_test.go | 167 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/v3/api/client_test.go b/v3/api/client_test.go index 0bbc14e..f256d40 100644 --- a/v3/api/client_test.go +++ b/v3/api/client_test.go @@ -23,6 +23,7 @@ import ( "net/http/httptest" "net/url" "os" + "sync" "sync/atomic" "testing" "time" @@ -282,3 +283,169 @@ func TestClient_ReusesHttpClientAcrossRequests(t *testing.T) { ) } } + +// TestClient_ConcurrentRequestsNotCappedByMaxConnsPerHost is an end-to-end +// regression test closing the loop on a finding this package's own +// http.Client-caching fix (see TestClient_ReusesHttpClientAcrossRequests) +// caused but could not fix locally: caching a single *http.Client means the +// transport keyfactor-auth-client-go builds is now reused for the lifetime +// of the Client instance, so any nonzero MaxConnsPerHost on that transport +// stops being a harmless per-request default and becomes a permanent, +// unqueued-timeout ceiling on concurrent in-flight requests for the whole +// process - e.g. `terraform apply -parallelism=25` would silently serialize +// into batches of N with no bound on how long excess requests wait, since +// neither the cached client's Timeout (0, unset) nor its requests' contexts +// impose one. +// +// keyfactor-auth-client-go previously hardcoded MaxConnsPerHost: 10 on this +// transport. Its own fix (auth_core.go's newHTTPTransport, now pinned at 0 / +// unbounded to match net/http.DefaultTransport) was verified from that +// repo's side by inspecting the constructed *http.Transport's field value. +// This test verifies the fix end-to-end from this repo's perspective instead +// of trusting that inspection alone: it builds a real Client via +// NewKeyfactorClient (exactly as production code does), retrieves its cached +// *http.Client via getHttpClient(), and drives 25 concurrent requests through +// it against a real httptest server, asserting the server actually observes +// well more than 10 requests in flight at once rather than serializing into +// batches of 10. +func TestClient_ConcurrentRequestsNotCappedByMaxConnsPerHost(t *testing.T) { + const ( + concurrentRequests = 25 + holdDuration = 200 * time.Millisecond + ) + + var ( + mu sync.Mutex + inFlight int + maxInFlight int + ) + + srv := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // The initial CommandAuthConfigBasic.Authenticate() call made + // by NewKeyfactorClient below hits this same handler; letting + // it fall through the same slow path is harmless since it + // happens once, sequentially, before the concurrent phase + // starts timing anything. + mu.Lock() + inFlight++ + if inFlight > maxInFlight { + maxInFlight = inFlight + } + mu.Unlock() + + time.Sleep(holdDuration) + + mu.Lock() + inFlight-- + mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("x-keyfactor-product-version", "99.9.9") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + }, + ), + ) + t.Cleanup(srv.Close) + + u, uErr := url.Parse(srv.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + cfg := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + } + + ctx := context.Background() + client, err := NewKeyfactorClient(cfg, &ctx) + if err != nil { + t.Fatalf("NewKeyfactorClient failed: %v", err) + } + + httpClient, hErr := client.getHttpClient() + if hErr != nil { + t.Fatalf("getHttpClient failed: %v", hErr) + } + + // Reset the counters: the single sequential Authenticate() call above + // already touched inFlight/maxInFlight and this resets the baseline so + // the assertion below reflects only the concurrent phase. + mu.Lock() + inFlight = 0 + maxInFlight = 0 + mu.Unlock() + + start := time.Now() + var wg sync.WaitGroup + for i := 0; i < concurrentRequests; i++ { + wg.Add(1) + go func() { + defer wg.Done() + req, rErr := http.NewRequest(http.MethodGet, srv.URL+"/KeyfactorAPI/concurrent-probe", nil) + if rErr != nil { + t.Errorf("failed to build request: %v", rErr) + return + } + resp, dErr := httpClient.Do(req) + if dErr != nil { + t.Errorf("request failed: %v", dErr) + return + } + _ = resp.Body.Close() + }() + } + wg.Wait() + elapsed := time.Since(start) + + mu.Lock() + observedMax := maxInFlight + mu.Unlock() + + t.Logf( + "observed max in-flight requests: %d/%d, wall time: %s (old MaxConnsPerHost=10 cap measured ~10 in-flight/909ms for a comparable batch; unbounded measured ~315ms)", + observedMax, + concurrentRequests, + elapsed, + ) + + // The old hardcoded MaxConnsPerHost: 10 would cap this at exactly 10 + // no matter how many requests are fired concurrently. Assert well above + // that ceiling (comfortably below concurrentRequests to tolerate + // scheduler jitter) to prove the requests are not being serialized into + // batches of 10. + const minAcceptableMaxInFlight = 15 + if observedMax <= 10 { + t.Fatalf( + "expected max concurrent in-flight requests to exceed the old MaxConnsPerHost=10 ceiling, got %d (elapsed %s) - concurrency ceiling regression", + observedMax, + elapsed, + ) + } + if observedMax < minAcceptableMaxInFlight { + t.Fatalf( + "expected max concurrent in-flight requests to be close to %d (unbounded), got only %d (elapsed %s)", + concurrentRequests, + observedMax, + elapsed, + ) + } + + // Wall time is a secondary signal: fully serialized into batches of 10 + // would take ceil(25/10)*holdDuration ~= 3*200ms = 600ms; unbounded + // concurrency should complete in roughly one holdDuration plus overhead. + maxAcceptableElapsed := holdDuration * 2 + if elapsed > maxAcceptableElapsed { + t.Fatalf( + "expected wall time close to a single %s hold duration for unbounded concurrency, got %s (elapsed too long, suggests serialization)", + holdDuration, + elapsed, + ) + } +} From 95b7380701f5ca1754cc2caf63dee90cc87a98ce Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:44:24 -0700 Subject: [PATCH 8/9] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.4 Picks up the round 5-6 OAuth token-fetch timeout hardening (bounded TCP dial phase and overall call during Configure), discovered via live-lab investigation after the branch had already converged once. No public API surface used by this module changed. --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index b5760c4..e0da278 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 diff --git a/v3/go.sum b/v3/go.sum index 93efff2..2adfd56 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 h1:1j0ZVOmay13SrpQkXrfaGBml8pEAsE7sDJzHHK6C2+Y= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 h1:pDKfmVk74gRjwtqtz7khMMM+sc6SxQAfcla+f7Q3ZuY= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 6079971b7bd3f9dee6e9d48042cc0adc5b2b3a9c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:09:13 -0700 Subject: [PATCH 9/9] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.5 Picks up the OAuth client_credentials token-fetch fix: avoid a redundant double round trip from AuthStyle probing and share a single deadline across retry attempts instead of a fresh timeout budget per attempt. --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index e0da278..705858a 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 diff --git a/v3/go.sum b/v3/go.sum index 2adfd56..df302cb 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 h1:pDKfmVk74gRjwtqtz7khMMM+sc6SxQAfcla+f7Q3ZuY= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 h1:nsp5hrG7EtGFOAaAIyRHt7FSLSWJSIDz66GrLaJU4yA= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=