From 01afb6612cdbdca31967245ce3b3225055f9f17f Mon Sep 17 00:00:00 2001 From: gyanranjanpanda Date: Sat, 23 May 2026 21:56:23 +0530 Subject: [PATCH 1/2] fix logout named contexts Signed-off-by: gyanranjanpanda --- cmd/logout.go | 50 +++++++++++++++++++-------------- cmd/logout_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 cmd/logout_test.go diff --git a/cmd/logout.go b/cmd/logout.go index 37351fb1..b1c638cc 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -7,7 +7,6 @@ import ( "github.com/microcks/microcks-cli/pkg/config" "github.com/microcks/microcks-cli/pkg/connectors" - "github.com/microcks/microcks-cli/pkg/errors" "github.com/spf13/cobra" ) @@ -29,29 +28,40 @@ microcks logout dev-context`, os.Exit(1) } - context := args[0] - localCfg, err := config.ReadLocalConfig(globalClientOpts.ConfigPath) - errors.CheckError(err) - if localCfg == nil { - log.Fatalf("Nothing to logout from") - } - - // Remove authToken - ok := localCfg.RemoveToken(context) - if !ok { - log.Fatalf("Context %s does not exist", context) - } - - err = config.ValidateLocalConfig(*localCfg) + target := args[0] + err := logoutContext(target, globalClientOpts.ConfigPath) if err != nil { - log.Fatalf("Error in loging out: %s", err) + log.Fatal(err) } - err = config.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath) - errors.CheckError(err) - - fmt.Printf("Logged out from '%s'\n", context) + fmt.Printf("Logged out from '%s'\n", target) }, } return logoutCmd } + +func logoutContext(target, configPath string) error { + localCfg, err := config.ReadLocalConfig(configPath) + if err != nil { + return err + } + if localCfg == nil { + return fmt.Errorf("Nothing to logout from") + } + + userName := target + if ctx, err := localCfg.ResolveContext(target); err == nil { + userName = ctx.User.Name + } + + if ok := localCfg.RemoveToken(userName); !ok { + return fmt.Errorf("Context %s does not exist", target) + } + + err = config.ValidateLocalConfig(*localCfg) + if err != nil { + return fmt.Errorf("Error in loging out: %s", err) + } + + return config.WriteLocalConfig(*localCfg, configPath) +} diff --git a/cmd/logout_test.go b/cmd/logout_test.go new file mode 100644 index 00000000..2801f445 --- /dev/null +++ b/cmd/logout_test.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/microcks/microcks-cli/pkg/config" + "github.com/stretchr/testify/require" +) + +func TestLogoutContextResolvesNamedContextUser(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config") + server := "https://microcks.example" + + localCfg := config.LocalConfig{ + CurrentContext: "staging", + Contexts: []config.ContextRef{ + {Name: "staging", Server: server, User: server}, + }, + Servers: []config.Server{ + {Server: server, KeycloakEnable: true}, + }, + Users: []config.User{ + {Name: server, AuthToken: "access-token", RefreshToken: "refresh-token"}, + }, + } + require.NoError(t, config.WriteLocalConfig(localCfg, configPath)) + + require.NoError(t, logoutContext("staging", configPath)) + + updated, err := config.ReadLocalConfig(configPath) + require.NoError(t, err) + require.NotNil(t, updated) + + user, err := updated.GetUser(server) + require.NoError(t, err) + require.Empty(t, user.AuthToken) + require.Empty(t, user.RefreshToken) +} + +func TestLogoutContextStillAcceptsStoredUserName(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config") + server := "https://microcks.example" + + localCfg := config.LocalConfig{ + CurrentContext: "staging", + Contexts: []config.ContextRef{ + {Name: "staging", Server: server, User: server}, + }, + Servers: []config.Server{ + {Server: server, KeycloakEnable: true}, + }, + Users: []config.User{ + {Name: server, AuthToken: "access-token", RefreshToken: "refresh-token"}, + }, + } + require.NoError(t, config.WriteLocalConfig(localCfg, configPath)) + + require.NoError(t, logoutContext(server, configPath)) + + updated, err := config.ReadLocalConfig(configPath) + require.NoError(t, err) + require.NotNil(t, updated) + + user, err := updated.GetUser(server) + require.NoError(t, err) + require.Empty(t, user.AuthToken) + require.Empty(t, user.RefreshToken) +} From 0f8b000f54b5679018773fd90a437139fbfdc29c Mon Sep 17 00:00:00 2001 From: gyanranjanpanda Date: Thu, 28 May 2026 09:33:47 +0530 Subject: [PATCH 2/2] fix: remove OAuth token logging and redact sensitive data from CLI output Remove unconditional log.Printf calls that leaked access tokens and refresh tokens to stderr after SSO login. Redact the callback URL (which contained the OAuth authorization code) and the authorization URL (which contained state nonce and code challenge). Additionally, add redaction of Authorization headers and OAuth token parameters in verbose HTTP dump output to prevent credential exposure when --verbose flag is used. Closes #449 Signed-off-by: gyanranjanpanda --- cmd/login.go | 6 ++++-- pkg/config/config.go | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 17bdb102..2180504e 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -219,7 +219,7 @@ func oauth2login( // Authorization redirect callback from OAuth2 auth flow. // Handles both implicit and authorization code flow callbackHandler := func(w http.ResponseWriter, r *http.Request) { - log.Printf("Callback: %s\n", r.URL) + log.Printf("Callback received on: %s\n", r.URL.Path) if formErr := r.FormValue("error"); formErr != "" { handleErr(w, fmt.Sprintf("%s: %s", formErr, r.FormValue("error_description"))) @@ -276,7 +276,8 @@ func oauth2login( opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", "S256")) url = oauth2conf.AuthCodeURL(stateNonce, opts...) - fmt.Printf("Performing %s flow login: %s\n", "authorization_code", url) + authBaseURL := strings.SplitN(url, "?", 2)[0] + fmt.Printf("Performing %s flow login: %s\n", "authorization_code", authBaseURL) time.Sleep(1 * time.Second) ssoAuthFlow(url, ssoLaunchBrowser) go func() { @@ -293,6 +294,7 @@ func oauth2login( ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() _ = srv.Shutdown(ctx) + return tokenString, refreshToken } diff --git a/pkg/config/config.go b/pkg/config/config.go index e5ff9a98..2c473f8b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,7 +23,8 @@ import ( "net/http/httputil" "os" "path/filepath" - strings "strings" + "regexp" + "strings" ) var ( @@ -37,6 +38,13 @@ var ( ConfigPath = filepath.Join(os.Getenv("HOME"), ".microcks-cli", "config.yaml") ) +var sensitiveHeaderPattern = regexp.MustCompile( + `(?im)^(Authorization:\s*)(Bearer\s+)?(.+)$`, +) +var sensitiveParamPattern = regexp.MustCompile( + `(?i)(access_token|refresh_token|id_token|code)=([^&\s]+)`, +) + // CreateTLSConfig wraps the creation of tls.Config object for use with HTTP Client for example. func CreateTLSConfig() *tls.Config { tlsConfig := &tls.Config{} @@ -76,7 +84,7 @@ func DumpRequestIfRequired(name string, req *http.Request, body bool) { if err != nil { fmt.Println("Got error while dumping request out") } - fmt.Printf("%s", dump) + fmt.Printf("%s", redactSensitiveContent(string(dump))) } } @@ -88,9 +96,16 @@ func DumpResponseIfRequired(name string, resp *http.Response, body bool) { if err != nil { fmt.Println("Got error while dumping response") } - fmt.Printf("%s", dump) + fmt.Printf("%s", redactSensitiveContent(string(dump))) if body { fmt.Println("") } } } + +// redactSensitiveContent masks OAuth tokens and credentials in HTTP dump output. +func redactSensitiveContent(dump string) string { + redacted := sensitiveHeaderPattern.ReplaceAllString(dump, "${1}[REDACTED]") + redacted = sensitiveParamPattern.ReplaceAllString(redacted, "${1}=[REDACTED]") + return redacted +}