From 7886f2ad17c26bce43e31a4677c5b7308ccb2b69 Mon Sep 17 00:00:00 2001 From: Ed Umansky Date: Wed, 19 Aug 2026 13:18:56 -0600 Subject: [PATCH 1/2] Fix missing tokens on updates and add command to view token --- cmd/auth.go | 15 +++++++++++++++ cmd/root.go | 39 +++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/cmd/auth.go b/cmd/auth.go index 640e911..ade06fb 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -21,6 +21,20 @@ var authCmd = &cobra.Command{ Long: "View and manage authenticated users, check token status, and switch between users.", } +var authTokenCmd = &cobra.Command{ + Use: "token", + Short: "Print the current access token", + Long: "Print the access token for the active user, refreshing it if expired. Useful for making manual API calls with curl.", + RunE: func(cmd *cobra.Command, args []string) error { + tok := config.Token() + if tok == "" { + return fmt.Errorf("no access token available; run: webex login") + } + fmt.Println(tok) + return nil + }, +} + var authStatusCmd = &cobra.Command{ Use: "status", Short: "Show current authentication status", @@ -323,6 +337,7 @@ var authClearFolderDefaultCmd = &cobra.Command{ } func init() { + authCmd.AddCommand(authTokenCmd) authCmd.AddCommand(authStatusCmd) authCmd.AddCommand(authListCmd) authCmd.AddCommand(authSwitchCmd) diff --git a/cmd/root.go b/cmd/root.go index c17091e..dad88ee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -221,30 +221,29 @@ func init() { }) } -// skipAuth returns true for commands that don't need authentication. +// skipAuth returns true for commands that don't need authentication. Only the +// top-level name is matched: generated API subcommands reuse names from the +// exempt list ("update", "login", "logout") and do need a token. func skipAuth(cmd *cobra.Command) bool { - // Walk up to find the root-level command name - name := cmd.Name() - - // Check the command itself and all parents - for c := cmd; c != nil; c = c.Parent() { - switch c.Name() { - case "login", "logout", "auth", "config", "version", "update", "post-install", "help", "webex": - // "webex" is the root — only skip if it's the actual command being run (bare `webex`) - if c.Name() == "webex" { - continue - } - // set-org needs a token to validate the org, so don't skip auth - if c.Name() == "auth" && cmd.Name() == "set-org" { - continue - } - return true - } + if !cmd.HasParent() { + return true + } + switch cmd.Name() { + case "help", "completion", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd: + return true + } + + top := cmd + for top.Parent().HasParent() { + top = top.Parent() } - // Also skip bare root command and help - if name == "help" || name == "webex" { + switch top.Name() { + case "login", "logout", "config", "version", "update", "post-install", "help", "completion": return true + case "auth": + // set-org validates the org against the API; token prints it. + return cmd.Name() != "set-org" && cmd.Name() != "token" } return false From 9cec952d47fcc859f72f9e33bee06745e86d8518 Mon Sep 17 00:00:00 2001 From: Ed Umansky Date: Wed, 19 Aug 2026 13:22:34 -0600 Subject: [PATCH 2/2] Show redacted Authorization header length in --debug output --debug omitted the Authorization header entirely, so a request sent with an empty bearer token looked identical to a correctly authenticated one. Print the scheme and credential length instead of hiding the header. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KJEaYFr7zMmqbF6j1mZFT3 --- internal/client/client.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/client/client.go b/internal/client/client.go index f7b2a6b..b2b3e06 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -90,6 +90,18 @@ func retryAfterDuration(header string) time.Duration { return 5 * time.Second } +// redactHeader replaces a credential value with its length, which is enough to +// tell a missing or truncated token from a present one without printing it. +func redactHeader(key, value string) string { + if !strings.EqualFold(key, "Authorization") { + return value + } + if scheme, cred, ok := strings.Cut(value, " "); ok { + return fmt.Sprintf("%s <%d chars>", scheme, len(cred)) + } + return fmt.Sprintf("<%d chars>", len(value)) +} + // doOnce executes a single HTTP request without retry. func doOnce(req *Request) ([]byte, int, http.Header, error) { // Build URL @@ -140,9 +152,7 @@ func doOnce(req *Request) ([]byte, int, http.Header, error) { if config.Debug() { fmt.Fprintf(os.Stderr, "DEBUG: %s %s\n", req.method, url) for k, v := range httpReq.Header { - if k != "Authorization" { - fmt.Fprintf(os.Stderr, "DEBUG: %s: %s\n", k, strings.Join(v, ", ")) - } + fmt.Fprintf(os.Stderr, "DEBUG: %s: %s\n", k, redactHeader(k, strings.Join(v, ", "))) } if req.bodyRaw != "" { fmt.Fprintf(os.Stderr, "DEBUG: Body: %s\n", truncate(req.bodyRaw, 500)) @@ -153,9 +163,7 @@ func doOnce(req *Request) ([]byte, int, http.Header, error) { if config.DryRun() && isWriteMethod(req.method) { fmt.Fprintf(os.Stderr, "[DRY RUN] %s %s\n", req.method, url) for k, v := range httpReq.Header { - if k != "Authorization" { - fmt.Fprintf(os.Stderr, "[DRY RUN] %s: %s\n", k, strings.Join(v, ", ")) - } + fmt.Fprintf(os.Stderr, "[DRY RUN] %s: %s\n", k, redactHeader(k, strings.Join(v, ", "))) } if req.bodyRaw != "" { fmt.Fprintf(os.Stderr, "[DRY RUN] Body: %s\n", req.bodyRaw)