From 3f2ad8bf059d8a3cea0bd7a9cfbf1c8d51dab39b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Oct 2026 16:29:00 +0200 Subject: [PATCH 1/5] internal/oauth: avoid opening browser after login cancellation Check the context after reading stdin to avoid launching the browser if login was canceled while waiting for ENTER. Signed-off-by: Sebastiaan van Stijn --- internal/oauth/manager/manager.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 96deb61b1325..63af81128f58 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -83,6 +83,9 @@ var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login // The retrieved tokens are stored in the credentials store (under a separate // key), and the refresh token is concatenated with the client ID. func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + state, err := m.api.GetDeviceCode(ctx, m.audience) if err != nil { logrus.Debugf("failed to start device code login: %v", err) @@ -120,7 +123,9 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut go func() { reader := bufio.NewReader(os.Stdin) - _, _ = reader.ReadString('\n') + if _, err := reader.ReadString('\n'); err != nil || ctx.Err() != nil { + return + } _ = m.openBrowser(state.VerificationURI) }() From ae317ab44a9075fff6baee8a8cdb4d6806fc9846 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Oct 2026 16:43:18 +0200 Subject: [PATCH 2/5] internal/oauth: combine "web-based login" message with info note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the output less verbose, and combine the "web-based login" message with the info message that's already printed. Before this patch: docker login USING WEB-BASED LOGIN i Info → To sign in with credentials on the command line, use 'docker login -u ' Your one-time device confirmation code is: VVPV-NSSL Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate Waiting for authentication in the browser… With this patch: docker login i Info → Using web-based login. To sign in with credentials on the command line, use 'docker login -u ' Your one-time device confirmation code is: QBBD-PQNS Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate Waiting for authentication in the browser… Signed-off-by: Sebastiaan van Stijn --- docs/reference/commandline/login.md | 3 +-- e2e/registry/login_test.go | 2 +- internal/oauth/manager/manager.go | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index a4e443bcaabd..b5e03c487dab 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -180,8 +180,7 @@ code, and authenticate. ```console $ docker login -USING WEB-BASED LOGIN -To sign in with credentials on the command line, use 'docker login -u ' +Using web-based login. To sign in with credentials on the command line, use 'docker login -u ' Your one-time device confirmation code is: LNFR-PGCJ Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate diff --git a/e2e/registry/login_test.go b/e2e/registry/login_test.go index 0c53cf12683e..3c9c18cf9df9 100644 --- a/e2e/registry/login_test.go +++ b/e2e/registry/login_test.go @@ -30,5 +30,5 @@ func TestOauthLogin(t *testing.T) { assert.NilError(t, err) output, _ := io.ReadAll(p) - assert.Check(t, strings.Contains(string(output), "USING WEB-BASED LOGIN"), string(output)) + assert.Check(t, strings.Contains(string(output), "Using web-based login"), string(output)) } diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 63af81128f58..9997ed5982c1 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -97,8 +97,6 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut return nil, ErrDeviceLoginStartFail } - _, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB-BASED LOGIN")) - var out tui.Output switch stream := w.(type) { case *streams.Out: @@ -106,7 +104,7 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut default: out = tui.NewOutput(streams.NewOut(w)) } - out.PrintNote("To sign in with credentials on the command line, use 'docker login -u '\n") + out.PrintNote("Using web-based login. To sign in with credentials on the command line, use 'docker login -u '\n") _, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode) _, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0]) From dc66c2a8d0efa8e358f8a48eccfabffd5c23ad93 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Oct 2026 17:05:30 +0200 Subject: [PATCH 3/5] internal/oauth: handle error when failing to open browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, pressing `ENTER` to open the browser would silently discard the error; docker login i Info → Using web-based login. To sign in with credentials on the command line, use 'docker login -u ' Your one-time device confirmation code is: VGTB-VKBG Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate Waiting for authentication in the browser… With this patch, a failure is printed, so that the user can manually open the URL (or cancel the login); docker login i Info → Using web-based login. To sign in with credentials on the command line, use 'docker login -u ' Your one-time device confirmation code is: SZFX-BSGP Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate Waiting for authentication in the browser… No browser found. Open the URL above manually to continue. Signed-off-by: Sebastiaan van Stijn --- internal/oauth/manager/manager.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 9997ed5982c1..37bce932fa27 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "os/exec" "strings" "github.com/docker/cli/cli/config/credentials" @@ -124,7 +125,19 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut if _, err := reader.ReadString('\n'); err != nil || ctx.Err() != nil { return } - _ = m.openBrowser(state.VerificationURI) + err := m.openBrowser(state.VerificationURI) + if ctx.Err() != nil { + return + } + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + _, _ = fmt.Fprintln(w, "No browser found. Open the URL above manually to continue.") + } else { + _, _ = fmt.Fprintln(w, "Failed to open browser:", err) + _, _ = fmt.Fprintln(w, "Open the URL above manually to continue.") + } + return + } }() _, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n") From fa65e9a3a04b741164a66a5ce5a25b8ef64f184e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Oct 2026 18:13:04 +0200 Subject: [PATCH 4/5] internal/oauth: cleanup output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove extra newline before message - Include the token in the URL (instead of requiring the user to manually enter it on the website). - Improve the "call to action"; ENTER can be used to automatically open the browser (but the user can manually open it instead), and CTRL-C can be used to cancel the login. - Remove the "Waiting for authentication in the browser…" message, which could be confusing if no browser was opened (yet), instead print a message after successfully requesting the browser to be opened. When running `docker login`; docker login i Info → Using web-based login. To sign in with credentials on the command line, use 'docker login -u ' Your one-time device confirmation code is: NRJL-BLRL Open this URL to authenticate: https://login.docker.com/activate?user_code=NRJL-BLRL Press ENTER to open the URL in your browser, or Ctrl+C to cancel. After pressing `ENTER`, it either prints the "success" message; Press ENTER to open the URL in your browser, or Ctrl+C to cancel. Opening browser… or a failure when unable to launch a browser; Press ENTER to open the URL in your browser, or Ctrl+C to cancel. No browser found. Open the URL above manually to continue. Signed-off-by: Sebastiaan van Stijn --- docs/reference/commandline/login.md | 4 ++-- internal/oauth/manager/manager.go | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index b5e03c487dab..458edd7b91b0 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -183,9 +183,9 @@ $ docker login Using web-based login. To sign in with credentials on the command line, use 'docker login -u ' Your one-time device confirmation code is: LNFR-PGCJ -Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate +Open this URL to authenticate: https://login.docker.com/activate?user_code=LNFR-PGCJ -Waiting for authentication in the browser… +Press ENTER to open the URL in your browser, or Ctrl+C to cancel. ``` After entering the code in your browser, you are authenticated to Docker Hub diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 37bce932fa27..77d17d04182a 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -106,8 +106,9 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut out = tui.NewOutput(streams.NewOut(w)) } out.PrintNote("Using web-based login. To sign in with credentials on the command line, use 'docker login -u '\n") - _, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode) - _, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0]) + _, _ = fmt.Fprintln(w, "Your one-time device confirmation code is:", aec.Bold.Apply(state.UserCode)) + _, _ = fmt.Fprintln(w, "Open this URL to authenticate:", aec.Underline.Apply(state.VerificationURI)) + _, _ = fmt.Fprintln(w, "\n"+aec.Bold.Apply("Press ENTER")+" to open the URL in your browser, or "+aec.Bold.Apply("Ctrl+C")+" to cancel.") tokenResChan := make(chan api.TokenResponse) waitForTokenErrChan := make(chan error) @@ -138,9 +139,9 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut } return } + _, _ = fmt.Fprintln(w, "\nOpening browser…") }() - _, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n") var tokenRes api.TokenResponse select { case <-ctx.Done(): From df9a0f430fa844ea674f9e8d01cc75c97adbd4aa Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Oct 2026 18:27:36 +0200 Subject: [PATCH 5/5] internal/oauth: remove debug logs, and move them to the caller Wrap errors in the OAuth manager instead of logging them, preserving the underlying cause while allowing callers to detect startup failures. Log failures at the fallback handler and scope the device-code login result to its branch. Signed-off-by: Sebastiaan van Stijn --- cli/command/registry/login.go | 12 ++++++------ internal/oauth/manager/manager.go | 8 ++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 471e73f8238e..2b522910d800 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -19,6 +19,7 @@ import ( "github.com/docker/cli/internal/tui" registrytypes "github.com/moby/moby/api/types/registry" "github.com/moby/moby/client" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -226,7 +227,7 @@ func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, auth return resp.Auth.Status, err } -func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) { +func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (message string, _ error) { // Some links documenting this: // - https://code.google.com/archive/p/mintty/issues/56 // - https://github.com/docker/docker/issues/15272 @@ -240,13 +241,12 @@ func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, de // If we're logging into the index server and the user didn't provide a username or password, use the device flow if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" { - var err error - msg, err = loginWithDeviceCodeFlow(ctx, dockerCLI) - // if the error represents a failure to initiate the device-code flow, - // then we fallback to regular cli credentials login - if !errors.Is(err, manager.ErrDeviceLoginStartFail) { + msg, err := loginWithDeviceCodeFlow(ctx, dockerCLI) + if err == nil || !errors.Is(err, manager.ErrDeviceLoginStartFail) { return msg, err } + + logrus.WithError(err).Debug("failed to start web-based login") _, _ = fmt.Fprint(dockerCLI.Err(), "Failed to start web-based login - falling back to command line login...\n\n") } diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 77d17d04182a..9816f7de18a3 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -18,8 +18,6 @@ import ( "github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/tui" "github.com/morikuni/aec" - "github.com/sirupsen/logrus" - "github.com/pkg/browser" ) @@ -89,13 +87,11 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut state, err := m.api.GetDeviceCode(ctx, m.audience) if err != nil { - logrus.Debugf("failed to start device code login: %v", err) - return nil, ErrDeviceLoginStartFail + return nil, fmt.Errorf("%w: %w", ErrDeviceLoginStartFail, err) } if state.UserCode == "" { - logrus.Debugf("failed to start device code login: missing user code") - return nil, ErrDeviceLoginStartFail + return nil, fmt.Errorf("%w: missing user code", ErrDeviceLoginStartFail) } var out tui.Output