From 54469093d79b2cda4d77cf299bba6fbe05746a4c 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 764597650034083fe0d17d512dedf8fce0a1b7a5 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 | 8 ++++---- pkg/config/config.go | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index bd24415f..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,8 +294,7 @@ func oauth2login( ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() _ = srv.Shutdown(ctx) - log.Printf("Token: %s\n", tokenString) - log.Printf("Refresh Token: %s\n", refreshToken) + 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 +}