Skip to content

Commit 4ea35f0

Browse files
authored
Merge a927fe7 into 6d655d3
2 parents 6d655d3 + a927fe7 commit 4ea35f0

File tree

7 files changed

+285
-55
lines changed

7 files changed

+285
-55
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# v1.3.0
2+
3+
## Features
4+
- Add support for fetching an oauth2 token using the `client_credentials` grant type without connecting to Keyfactor Command.
5+
- Add placeholders for omitted `Authorization` header in the `curl` command string output in trace logging.
6+
7+
## Bug Fixes
8+
- Log `curl` command string at `trace` level after request is sent to include any transport mutations.
9+
10+
## Chores
11+
- Bump Go version to `1.24`.
12+
113
# v1.2.0
214

315
## Features

auth_providers/auth_core.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -501,19 +501,21 @@ func (c *CommandAuthConfig) Authenticate() error {
501501
}
502502

503503
c.HttpClient.Timeout = time.Duration(c.HttpClientTimeout) * time.Second
504-
curlStr, cErr := RequestToCurl(req)
505-
if cErr == nil {
504+
505+
cResp, cErr := c.HttpClient.Do(req)
506+
curlStr, curlErr := RequestToCurl(req)
507+
if curlErr == nil {
506508
log.Printf("[TRACE] curl command: %s", curlStr)
507509
}
508510

509-
cResp, cErr := c.HttpClient.Do(req)
510511
if cErr != nil {
511512
return cErr
512513
} else if cResp == nil {
513514
return fmt.Errorf("failed to authenticate, no response received from Keyfactor Command")
514515
}
515516

516517
defer cResp.Body.Close()
518+
log.Printf("[DEBUG] request to Keyfactor Command API returned status code %d", cResp.StatusCode)
517519

518520
// check if body is empty
519521
if cResp.Body == nil {
@@ -798,19 +800,56 @@ func RequestToCurl(req *http.Request) (string, error) {
798800
// Add headers
799801
for name, values := range req.Header {
800802
for _, value := range values {
803+
// check if is Authorization header and skip it
804+
if strings.EqualFold(name, "Authorization") {
805+
// check if basic auth and skip it
806+
if strings.HasPrefix(value, "Basic ") {
807+
// Remove credentials and put in env variables as placeholder
808+
log.Printf(
809+
"[DEBUG] Found Basic auth in Authorization header, " +
810+
"replacing with env variable references",
811+
)
812+
curlCommand.WriteString(
813+
fmt.Sprintf(
814+
"-H %q ", fmt.Sprintf(
815+
"%s: Basic $(echo -n $\"%s,$%s\" | base64)", name,
816+
EnvKeyfactorUsername, EnvKeyfactorPassword,
817+
),
818+
),
819+
)
820+
continue
821+
} else if strings.HasPrefix(value, "Bearer ") {
822+
// Remove credentials and put in env variables as placeholder
823+
log.Printf("[DEBUG] Found Bearer token in Authorization header, replacing with kfutil command to fetch token")
824+
curlCommand.WriteString(
825+
fmt.Sprintf(
826+
"-H %q ", fmt.Sprintf(
827+
"%s: Bearer $(kfutil auth fetch-oauth-token)", name,
828+
),
829+
),
830+
)
831+
continue
832+
} else {
833+
// Skip other Authorization headers
834+
log.Printf("[ERROR] Skipping unhandled Authorization header: %s", name)
835+
continue
836+
}
837+
}
801838
curlCommand.WriteString(fmt.Sprintf("-H %q ", fmt.Sprintf("%s: %s", name, value)))
802839
}
803840
}
804841

805842
// Add the body if it exists
806843
if req.Method == http.MethodPost || req.Method == http.MethodPut {
807-
body, err := io.ReadAll(req.Body)
808-
if err != nil {
809-
return "", err
810-
}
811-
req.Body = io.NopCloser(bytes.NewBuffer(body)) // Restore the request body
844+
if req.Body != nil {
845+
body, err := io.ReadAll(req.Body)
846+
if err != nil {
847+
return "", err
848+
}
849+
req.Body = io.NopCloser(bytes.NewBuffer(body)) // Restore the request body
812850

813-
curlCommand.WriteString(fmt.Sprintf("--data %q ", string(body)))
851+
curlCommand.WriteString(fmt.Sprintf("--data %q ", string(body)))
852+
}
814853
}
815854

816855
return curlCommand.String(), nil

auth_providers/auth_core_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package auth_providers_test
1616

1717
import (
1818
"net/http"
19+
"strings"
1920
"testing"
2021

2122
"github.com/Keyfactor/keyfactor-auth-client-go/auth_providers"
@@ -102,3 +103,75 @@ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7Q2+1+2+1+2+1+2+1+2+
102103
t.Fatalf("expected non-zero blocks")
103104
}
104105
}
106+
107+
func TestRequestToCurl(t *testing.T) {
108+
tests := []struct {
109+
name string
110+
method string
111+
url string
112+
headers map[string]string
113+
wantInCurl []string
114+
notWantInCurl []string
115+
}{
116+
{
117+
name: "Basic Auth",
118+
method: "GET",
119+
url: "https://example.com/api",
120+
headers: map[string]string{
121+
"Authorization": "Basic dXNlcjpwYXNz",
122+
},
123+
wantInCurl: []string{
124+
"curl", "-X", "GET", "https://example.com/api",
125+
"-H", "Authorization: Basic",
126+
},
127+
notWantInCurl: []string{
128+
"Authorization: Basic dXNlcjpwYXNz",
129+
},
130+
},
131+
{
132+
name: "Bearer Auth",
133+
method: "POST",
134+
url: "https://example.com/token",
135+
headers: map[string]string{
136+
"Authorization": "Bearer testtoken",
137+
"Content-Type": "application/json",
138+
},
139+
wantInCurl: []string{
140+
"curl", "-X", "POST", "https://example.com/token",
141+
"-H", "Authorization: Bearer",
142+
"-H", "Content-Type: application/json",
143+
},
144+
notWantInCurl: []string{
145+
"Authorization: Bearer testtoken",
146+
},
147+
},
148+
}
149+
150+
for _, tt := range tests {
151+
req, err := http.NewRequest(tt.method, tt.url, nil)
152+
if err != nil {
153+
t.Fatalf("failed to create request: %v", err)
154+
}
155+
for k, v := range tt.headers {
156+
req.Header.Set(k, v)
157+
}
158+
159+
curlStr, err := auth_providers.RequestToCurl(req)
160+
if err != nil {
161+
t.Errorf("%s: RequestToCurl returned error: %v", tt.name, err)
162+
continue
163+
}
164+
for _, want := range tt.wantInCurl {
165+
if !strings.Contains(curlStr, want) {
166+
t.Errorf("%s: curl string missing %q\nGot: %s", tt.name, want, curlStr)
167+
}
168+
}
169+
170+
for _, notWant := range tt.notWantInCurl {
171+
if strings.Contains(curlStr, notWant) {
172+
t.Errorf("%s: curl string contains unwanted %q\nGot: %s", tt.name, notWant, curlStr)
173+
}
174+
}
175+
t.Logf("%s: curl command: %s", tt.name, curlStr)
176+
}
177+
}

auth_providers/auth_oauth.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"crypto/x509"
2020
"fmt"
21+
"log"
2122
"net/http"
2223
"os"
2324
"strings"
@@ -439,16 +440,80 @@ func (b *CommandConfigOauth) GetServerConfig() *Server {
439440
return &server
440441
}
441442

443+
// GetAccessToken returns the OAuth2 token source for the given configuration.
444+
func (b *CommandConfigOauth) GetAccessToken() (oauth2.TokenSource, error) {
445+
if b == nil {
446+
return nil, fmt.Errorf("CommandConfigOauth is nil")
447+
}
448+
449+
b.ValidateAuthConfig()
450+
451+
if b.AccessToken != "" && (b.ClientID == "" || b.ClientSecret == "" || b.TokenURL == "") {
452+
log.Printf("[DEBUG] Access token is explicitly set, and no client credentials are provided. Using static token source.")
453+
return oauth2.StaticTokenSource(
454+
&oauth2.Token{
455+
AccessToken: b.AccessToken,
456+
TokenType: DefaultTokenPrefix,
457+
Expiry: b.Expiry,
458+
},
459+
), nil
460+
}
461+
462+
log.Printf("[DEBUG] Getting OAuth2 token source for client ID: %s", b.ClientID)
463+
if b.ClientID == "" || b.ClientSecret == "" || b.TokenURL == "" {
464+
return nil, fmt.Errorf("client ID, client secret, and token URL must be provided")
465+
}
466+
467+
config := &clientcredentials.Config{
468+
ClientID: b.ClientID,
469+
ClientSecret: b.ClientSecret,
470+
TokenURL: b.TokenURL,
471+
Scopes: b.Scopes,
472+
}
473+
474+
if b.Audience != "" {
475+
log.Printf("[DEBUG] Setting audience for OAuth2 token source: %s", b.Audience)
476+
config.EndpointParams = map[string][]string{
477+
"audience": {b.Audience},
478+
}
479+
}
480+
481+
ctx := context.Background()
482+
log.Printf("[DEBUG] Returning call config.TokenSource() for client ID: %s", b.ClientID)
483+
tokenSource := config.TokenSource(ctx)
484+
if tokenSource == nil {
485+
return nil, fmt.Errorf("failed to create token source for client ID: %s", b.ClientID)
486+
}
487+
token, tErr := tokenSource.Token()
488+
if tErr != nil {
489+
return nil, fmt.Errorf("failed to retrieve token for client ID %s: %w", b.ClientID, tErr)
490+
}
491+
if token == nil || token.AccessToken == "" {
492+
return nil, fmt.Errorf("received empty OAuth token for client ID: %s", b.ClientID)
493+
}
494+
495+
return tokenSource, nil
496+
}
497+
442498
// RoundTrip executes a single HTTP transaction, adding the OAuth2 token to the request
443499
func (t *oauth2Transport) RoundTrip(req *http.Request) (*http.Response, error) {
500+
log.Printf("[DEBUG] Attempting to get oAuth token from: %s %s", req.Method, req.URL)
444501
token, err := t.src.Token()
445502
if err != nil {
503+
446504
return nil, fmt.Errorf("failed to retrieve OAuth token: %w", err)
447505
}
448506

507+
if token == nil || token.AccessToken == "" {
508+
return nil, fmt.Errorf("received empty OAuth token")
509+
}
510+
449511
// Clone the request to avoid mutating the original
512+
log.Printf("[DEBUG] Adding oAuth token to request: %s %s", req.Method, req.URL)
450513
reqCopy := req.Clone(req.Context())
451514
token.SetAuthHeader(reqCopy)
515+
requestCurlStr, _ := RequestToCurl(reqCopy)
516+
log.Printf("[TRACE] curl command: %s", requestCurlStr)
452517

453518
return t.base.RoundTrip(reqCopy)
454519
}

auth_providers/auth_oauth_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"testing"
2727

2828
"github.com/Keyfactor/keyfactor-auth-client-go/auth_providers"
29+
"golang.org/x/oauth2"
2930
)
3031

3132
func TestOAuthAuthenticator_GetHttpClient(t *testing.T) {
@@ -256,7 +257,7 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) {
256257
}
257258
fullParamsInvalidPassConfig.WithSkipVerify(true)
258259
invalidCredsExpectedError := []string{
259-
"oauth2", "unauthorized_client", "Invalid client or Invalid client credentials",
260+
"oauth2", "fail", "invalid", "client",
260261
}
261262
authOauthTest(t, "w/ full params & invalid pass", true, fullParamsInvalidPassConfig, invalidCredsExpectedError...)
262263

@@ -346,6 +347,18 @@ func TestCommandConfigOauth_Authenticate(t *testing.T) {
346347
authOauthTest(t, "with invalid creds implicit config file", true, invCmdHost, invHostExpectedError...)
347348
}
348349

350+
func TestCommandConfigOauth_GetAccessToken(t *testing.T) {
351+
clientID, clientSecret, tokenURL := exportOAuthEnvVariables()
352+
t.Log("Testing auth with w/ full params variables")
353+
fullParamsConfig := &auth_providers.CommandConfigOauth{
354+
ClientID: clientID,
355+
ClientSecret: clientSecret,
356+
TokenURL: tokenURL,
357+
}
358+
fullParamsConfig.WithSkipVerify(true)
359+
authOauthTest(t, "w/ GetAccessToken w/ full params variables", false, fullParamsConfig)
360+
}
361+
349362
func TestCommandConfigOauth_Build(t *testing.T) {
350363
// Skip test if TEST_KEYFACTOR_AD_AUTH is set to 1 or true
351364
if os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "1" || os.Getenv("TEST_KEYFACTOR_AD_AUTH") == "true" {
@@ -376,6 +389,34 @@ func authOauthTest(
376389
t.Run(
377390
fmt.Sprintf("oAuth Auth Test %s", testName), func(t *testing.T) {
378391

392+
// oauth credentials should always generate an access token
393+
oauthToken, tErr := config.GetAccessToken()
394+
if !allowFail {
395+
if tErr != nil {
396+
t.Errorf("oAuth auth test '%s' failed to get token source with %v", testName, tErr)
397+
t.FailNow()
398+
return
399+
}
400+
if oauthToken == nil {
401+
t.Errorf("oAuth auth test '%s' failed to get token source", testName)
402+
t.FailNow()
403+
return
404+
}
405+
var at *oauth2.Token
406+
var tkErr error
407+
at, tkErr = oauthToken.Token()
408+
if tkErr != nil {
409+
t.Errorf("oAuth auth test '%s' failed to get token source", testName)
410+
t.FailNow()
411+
}
412+
if at == nil || at.AccessToken == "" {
413+
t.Errorf("oAuth auth test '%s' failed to get token source", testName)
414+
t.FailNow()
415+
return
416+
}
417+
//t.Logf("token %s", at.AccessToken)
418+
t.Logf("oAuth auth test '%s' succeeded", testName)
419+
}
379420
err := config.Authenticate()
380421
if allowFail {
381422
if err == nil {

go.mod

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,32 @@
1414

1515
module github.com/Keyfactor/keyfactor-auth-client-go
1616

17-
go 1.23
17+
go 1.24.0
18+
19+
toolchain go1.24.3
1820

1921
require (
20-
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
21-
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0
22+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
23+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1
2224
github.com/stretchr/testify v1.10.0
23-
golang.org/x/oauth2 v0.25.0
25+
golang.org/x/oauth2 v0.30.0
2426
gopkg.in/yaml.v2 v2.4.0
2527
)
2628

2729
require (
28-
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
29-
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
30-
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect
31-
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
30+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
31+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
32+
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
33+
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
3234
github.com/davecgh/go-spew v1.1.1 // indirect
33-
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
35+
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
3436
github.com/google/uuid v1.6.0 // indirect
3537
github.com/kylelemons/godebug v1.1.0 // indirect
3638
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
3739
github.com/pmezard/go-difflib v1.0.0 // indirect
38-
golang.org/x/crypto v0.32.0 // indirect
39-
golang.org/x/net v0.34.0 // indirect
40-
golang.org/x/sys v0.29.0 // indirect
41-
golang.org/x/text v0.21.0 // indirect
40+
golang.org/x/crypto v0.39.0 // indirect
41+
golang.org/x/net v0.41.0 // indirect
42+
golang.org/x/sys v0.33.0 // indirect
43+
golang.org/x/text v0.26.0 // indirect
4244
gopkg.in/yaml.v3 v3.0.1 // indirect
4345
)

0 commit comments

Comments
 (0)