From 1e2da7043c26470a1553084af8cb24fc813be799 Mon Sep 17 00:00:00 2001 From: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:35:40 +0530 Subject: [PATCH 1/2] internal/oauth: use BROWSER env var to open the login URL The web-based login flow opens the device confirmation URL with github.com/pkg/browser, which only tries a fixed set of launchers (xdg-open, x-www-browser, www-browser on Linux). In environments such as dev containers, none of these may be installed, and users need a way to forward the URL to a browser on the host. Use the command from the BROWSER environment variable when it is set, following the same convention as other tools such as the GitHub CLI. If the command can't be started, fall back to the default browser. Signed-off-by: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com> --- docs/reference/commandline/login.md | 8 ++++++++ internal/oauth/manager/manager.go | 18 +++++++++++++++++- internal/oauth/manager/manager_test.go | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index a4e443bcaabd..215b610e36b8 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -189,6 +189,14 @@ Press ENTER to open your browser or submit your device code here: https://login. Waiting for authentication in the browser… ``` +Pressing ENTER opens the URL in your default browser. To use a +different command to open the URL, for example in a dev container that +forwards URLs to the host, set the `BROWSER` environment variable: + +```console +$ BROWSER="my-url-opener --flag" docker login +``` + After entering the code in your browser, you are authenticated to Docker Hub using the account you're currently signed in with on the Docker Hub website or in Docker Desktop. If you aren't signed in, you are prompted to sign in after diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 96deb61b1325..e6750b74c195 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" @@ -56,7 +57,7 @@ func New(options OAuthManagerOptions) *OAuthManager { // cluttering the output. We can handle errors ourselves. browser.Stdout = io.Discard browser.Stderr = io.Discard - openBrowser = browser.OpenURL + openBrowser = openURL } return &OAuthManager{ @@ -73,6 +74,21 @@ func New(options OAuthManagerOptions) *OAuthManager { } } +// openURL opens url using the command in the BROWSER environment variable +// if set, and falls back to the platform default otherwise, or if the +// command can't be started. +func openURL(url string) error { + if args := strings.Fields(os.Getenv("BROWSER")); len(args) > 0 { + cmd := exec.Command(args[0], append(args[1:], url)...) + err := cmd.Start() + if err == nil { + return cmd.Wait() + } + logrus.WithError(err).Debug("failed to run BROWSER command, using default browser") + } + return browser.OpenURL(url) +} + var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login") // LoginDevice launches the device authentication flow with the tenant, diff --git a/internal/oauth/manager/manager_test.go b/internal/oauth/manager/manager_test.go index 44a744999b8b..471052d3877d 100644 --- a/internal/oauth/manager/manager_test.go +++ b/internal/oauth/manager/manager_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "os" + "path/filepath" + "runtime" "testing" "time" @@ -361,3 +363,17 @@ func (*fakeStore) GetFilename() string { func newStore(auths map[string]types.AuthConfig) *fakeStore { return &fakeStore{configs: auths} } + +func TestOpenURLWithBrowserEnv(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("uses touch as the browser command") + } + // "touch -m " creates a file at , which lets us check that + // the command (with its arguments) from BROWSER was used to open the url. + t.Setenv("BROWSER", "touch -m") + url := filepath.Join(t.TempDir(), "opened") + + assert.NilError(t, openURL(url)) + _, err := os.Stat(url) + assert.NilError(t, err) +} From 40b474d9d6582fea2805296e8edf4a46060d5da1 Mon Sep 17 00:00:00 2001 From: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:54:47 +0530 Subject: [PATCH 2/2] internal/oauth: silence gosec G204 for BROWSER command The command is chosen by the user through the BROWSER environment variable, so launching it is the intended behavior. Signed-off-by: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com> --- internal/oauth/manager/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index e6750b74c195..85f7adfd9b5f 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -79,7 +79,7 @@ func New(options OAuthManagerOptions) *OAuthManager { // command can't be started. func openURL(url string) error { if args := strings.Fields(os.Getenv("BROWSER")); len(args) > 0 { - cmd := exec.Command(args[0], append(args[1:], url)...) + cmd := exec.Command(args[0], append(args[1:], url)...) // #nosec G204 -- BROWSER is set by the user to choose the command to run err := cmd.Start() if err == nil { return cmd.Wait()