From 14341edfc2bf61d3992145cd6e33ac0ef949248d Mon Sep 17 00:00:00 2001 From: Zach <3724288+zedkipp@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:57:27 -0600 Subject: [PATCH] fix(cli): fix `coder login token` failing without --url flag (#22742) Previously `coder login token` didn't load the server URL from config, so it always required --url or CODER_URL when using the keyring to store the session token. This command would only print out the token when already logged in to a deployment and file storage is used to store the session token (keyring is the default on Windows/macOS). It would also print out an incorrect token when --url was specified and the session token stored on disk was for a different deployment that the user logged into. This change fixes all of these issues, and also errors out when using session token file storage with a `--url` argument that doesn't match the stored config URL, since the file only stores one token and would silently return the wrong one. See https://github.com/coder/coder/issues/22733 for a table of the before/after behaviors. --- cli/login.go | 21 ++++++++++++++++++++- cli/login_test.go | 25 ++++++++++++++++++++++++- cli/root.go | 45 ++++++++++++++++++++++++--------------------- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/cli/login.go b/cli/login.go index 297fbedf3e..7fd6a74866 100644 --- a/cli/login.go +++ b/cli/login.go @@ -475,7 +475,26 @@ func (r *RootCmd) loginToken() *serpent.Command { Long: "Print the session token for use in scripts and automation.", Middleware: serpent.RequireNArgs(0), Handler: func(inv *serpent.Invocation) error { - tok, err := r.ensureTokenBackend().Read(r.clientURL) + if err := r.ensureClientURL(); err != nil { + return err + } + // When using the file storage, a session token is stored for a single + // deployment URL that the user is logged in to. They keyring can store + // multiple deployment session tokens. Error if the requested URL doesn't + // match the stored config URL when using file storage to avoid returning + // a token for the wrong deployment. + backend := r.ensureTokenBackend() + if _, ok := backend.(*sessionstore.File); ok { + conf := r.createConfig() + storedURL, err := conf.URL().Read() + if err == nil { + storedURL = strings.TrimSpace(storedURL) + if storedURL != r.clientURL.String() { + return xerrors.Errorf("file session token storage only supports one server at a time: requested %s but logged into %s", r.clientURL.String(), storedURL) + } + } + } + tok, err := backend.Read(r.clientURL) if err != nil { if xerrors.Is(err, os.ErrNotExist) { return xerrors.New("no session token found - run 'coder login' first") diff --git a/cli/login_test.go b/cli/login_test.go index 4125519e1f..5d1af88265 100644 --- a/cli/login_test.go +++ b/cli/login_test.go @@ -558,10 +558,33 @@ func TestLoginToken(t *testing.T) { t.Run("NoTokenStored", func(t *testing.T) { t.Parallel() - inv, _ := clitest.New(t, "login", "token") + client := coderdtest.New(t, nil) + inv, _ := clitest.New(t, "login", "token", "--url", client.URL.String()) ctx := testutil.Context(t, testutil.WaitShort) err := inv.WithContext(ctx).Run() require.Error(t, err) require.Contains(t, err.Error(), "no session token found") }) + + t.Run("NoURLProvided", func(t *testing.T) { + t.Parallel() + inv, _ := clitest.New(t, "login", "token") + ctx := testutil.Context(t, testutil.WaitShort) + err := inv.WithContext(ctx).Run() + require.Error(t, err) + require.Contains(t, err.Error(), "You are not logged in") + }) + + t.Run("URLMismatchFileBackend", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + coderdtest.CreateFirstUser(t, client) + + inv, root := clitest.New(t, "login", "token", "--url", "https://other.example.com") + clitest.SetupConfig(t, client, root) + ctx := testutil.Context(t, testutil.WaitShort) + err := inv.WithContext(ctx).Run() + require.Error(t, err) + require.Contains(t, err.Error(), "file session token storage only supports one server") + }) } diff --git a/cli/root.go b/cli/root.go index cf7504f7b9..aed58e4690 100644 --- a/cli/root.go +++ b/cli/root.go @@ -550,30 +550,33 @@ type RootCmd struct { useKeyringWithGlobalConfig bool } +// ensureClientURL loads the client URL from the config file if it +// wasn't provided via --url or CODER_URL. +func (r *RootCmd) ensureClientURL() error { + if r.clientURL != nil && r.clientURL.String() != "" { + return nil + } + rawURL, err := r.createConfig().URL().Read() + // If the configuration files are absent, the user is logged out. + if os.IsNotExist(err) { + binPath, err := os.Executable() + if err != nil { + binPath = "coder" + } + return xerrors.Errorf(notLoggedInMessage, binPath) + } + if err != nil { + return err + } + r.clientURL, err = url.Parse(strings.TrimSpace(rawURL)) + return err +} + // InitClient creates and configures a new client with authentication, telemetry, // and version checks. func (r *RootCmd) InitClient(inv *serpent.Invocation) (*codersdk.Client, error) { - conf := r.createConfig() - var err error - // Read the client URL stored on disk. - if r.clientURL == nil || r.clientURL.String() == "" { - rawURL, err := conf.URL().Read() - // If the configuration files are absent, the user is logged out - if os.IsNotExist(err) { - binPath, err := os.Executable() - if err != nil { - binPath = "coder" - } - return nil, xerrors.Errorf(notLoggedInMessage, binPath) - } - if err != nil { - return nil, err - } - - r.clientURL, err = url.Parse(strings.TrimSpace(rawURL)) - if err != nil { - return nil, err - } + if err := r.ensureClientURL(); err != nil { + return nil, err } if r.token == "" { tok, err := r.ensureTokenBackend().Read(r.clientURL)