Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/reference/commandline/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion internal/oauth/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/docker/cli/cli/config/credentials"
Expand Down Expand Up @@ -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{
Expand All @@ -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)...) // #nosec G204 -- BROWSER is set by the user to choose the command to run
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,
Expand Down
16 changes: 16 additions & 0 deletions internal/oauth/manager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"time"

Expand Down Expand Up @@ -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 <url>" creates a file at <url>, 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)
}