Skip to content

Commit 6d7afd4

Browse files
committed
login: improve text on already authenticated and on OAuth login
Users have trouble understanding the different login paths on the CLI. The default login is performed through an OAuth flow with the option to fallback to a username and PAT login using the docker login -u <username> option. This patch improves the text around docker login, indicating: - The username is shown when already authenticated - Steps the user can take to switch user accounts are printed when authenticated in an info. - When not authenticated, the OAuth login flow explains the fallback clearly to the user in an info. - The password prompt now explicitly states that it accepts a PAT in an info. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent 7c3fa81 commit 6d7afd4

5 files changed

Lines changed: 61 additions & 37 deletions

File tree

cli/command/registry.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import (
1313
configtypes "github.com/docker/cli/cli/config/types"
1414
"github.com/docker/cli/cli/hints"
1515
"github.com/docker/cli/cli/streams"
16+
"github.com/docker/cli/internal/tui"
1617
registrytypes "github.com/docker/docker/api/types/registry"
1718
"github.com/docker/docker/registry"
19+
"github.com/morikuni/aec"
1820
"github.com/pkg/errors"
1921
)
2022

@@ -178,6 +180,9 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
178180
}
179181
}()
180182

183+
out := tui.NewOutput(cli.Err())
184+
out.PrintNote("A Personal Access Token (PAT) can be used instead.\n" +
185+
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
181186
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
182187
if err != nil {
183188
return registrytypes.AuthConfig{}, err

cli/command/registry/login.go

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/docker/cli/cli/config/configfile"
1515
configtypes "github.com/docker/cli/cli/config/types"
1616
"github.com/docker/cli/cli/internal/oauth/manager"
17+
"github.com/docker/cli/internal/tui"
1718
registrytypes "github.com/docker/docker/api/types/registry"
1819
"github.com/docker/docker/client"
1920
"github.com/docker/docker/errdefs"
@@ -30,7 +31,7 @@ type loginOptions struct {
3031
}
3132

3233
// NewLoginCommand creates a new `docker login` command
33-
func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
34+
func NewLoginCommand(dockerCLI command.Cli) *cobra.Command {
3435
var opts loginOptions
3536

3637
cmd := &cobra.Command{
@@ -42,7 +43,7 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
4243
if len(args) > 0 {
4344
opts.serverAddress = args[0]
4445
}
45-
return runLogin(cmd.Context(), dockerCli, opts)
46+
return runLogin(cmd.Context(), dockerCLI, opts)
4647
},
4748
Annotations: map[string]string{
4849
"category-top": "8",
@@ -53,15 +54,15 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
5354
flags := cmd.Flags()
5455

5556
flags.StringVarP(&opts.user, "username", "u", "", "Username")
56-
flags.StringVarP(&opts.password, "password", "p", "", "Password")
57-
flags.BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the password from stdin")
57+
flags.StringVarP(&opts.password, "password", "p", "", "Password or Personal Access Token (PAT)")
58+
flags.BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the Password or Personal Access Token (PAT) from stdin")
5859

5960
return cmd
6061
}
6162

62-
func verifyLoginOptions(dockerCli command.Cli, opts *loginOptions) error {
63+
func verifyLoginOptions(dockerCLI command.Cli, opts *loginOptions) error {
6364
if opts.password != "" {
64-
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
65+
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
6566
if opts.passwordStdin {
6667
return errors.New("--password and --password-stdin are mutually exclusive")
6768
}
@@ -72,7 +73,7 @@ func verifyLoginOptions(dockerCli command.Cli, opts *loginOptions) error {
7273
return errors.New("Must provide --username with --password-stdin")
7374
}
7475

75-
contents, err := io.ReadAll(dockerCli.In())
76+
contents, err := io.ReadAll(dockerCLI.In())
7677
if err != nil {
7778
return err
7879
}
@@ -83,8 +84,8 @@ func verifyLoginOptions(dockerCli command.Cli, opts *loginOptions) error {
8384
return nil
8485
}
8586

86-
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
87-
if err := verifyLoginOptions(dockerCli, &opts); err != nil {
87+
func runLogin(ctx context.Context, dockerCLI command.Cli, opts loginOptions) error {
88+
if err := verifyLoginOptions(dockerCLI, &opts); err != nil {
8889
return err
8990
}
9091
var (
@@ -99,28 +100,38 @@ func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) err
99100
isDefaultRegistry := serverAddress == registry.IndexServer
100101

101102
// attempt login with current (stored) credentials
102-
authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
103+
authConfig, err := command.GetDefaultAuthConfig(dockerCLI.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
103104
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
104-
msg, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
105+
msg, err = loginWithStoredCredentials(ctx, dockerCLI, authConfig)
105106
}
106107

107108
// if we failed to authenticate with stored credentials (or didn't have stored credentials),
108109
// prompt the user for new credentials
109110
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
110-
msg, err = loginUser(ctx, dockerCli, opts, authConfig.Username, authConfig.ServerAddress)
111+
msg, err = loginUser(ctx, dockerCLI, opts, authConfig.Username, authConfig.ServerAddress)
111112
if err != nil {
112113
return err
113114
}
114115
}
115116

116117
if msg != "" {
117-
_, _ = fmt.Fprintln(dockerCli.Out(), msg)
118+
_, _ = fmt.Fprintln(dockerCLI.Out(), msg)
118119
}
119120
return nil
120121
}
121122

122123
func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, authConfig registrytypes.AuthConfig) (msg string, _ error) {
123-
_, _ = fmt.Fprintln(dockerCLI.Out(), "Authenticating with existing credentials...")
124+
_, _ = fmt.Fprintf(dockerCLI.Err(), "Authenticating with existing credentials...")
125+
if authConfig.Username != "" {
126+
_, _ = fmt.Fprintf(dockerCLI.Err(), " [Username: %s]", authConfig.Username)
127+
}
128+
_, _ = fmt.Fprint(dockerCLI.Err(), "\n")
129+
130+
out := tui.NewOutput(dockerCLI.Err())
131+
out.PrintNote("To login with a different account, run 'docker logout' followed by 'docker login'")
132+
133+
_, _ = fmt.Fprint(dockerCLI.Err(), "\n\n")
134+
124135
response, err := dockerCLI.Client().RegistryLogin(ctx, authConfig)
125136
if err != nil {
126137
if errdefs.IsUnauthorized(err) {
@@ -155,41 +166,41 @@ func isOauthLoginDisabled() bool {
155166
return false
156167
}
157168

158-
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
169+
func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
159170
// Some links documenting this:
160171
// - https://code.google.com/archive/p/mintty/issues/56
161172
// - https://github.com/docker/docker/issues/15272
162173
// - https://mintty.github.io/ (compatibility)
163174
// Linux will hit this if you attempt `cat | docker login`, and Windows
164175
// will hit this if you attempt docker login from mintty where stdin
165176
// is a pipe, not a character based console.
166-
if (opts.user == "" || opts.password == "") && !dockerCli.In().IsTerminal() {
177+
if (opts.user == "" || opts.password == "") && !dockerCLI.In().IsTerminal() {
167178
return "", errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
168179
}
169180

170181
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
171182
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() {
172183
var err error
173-
msg, err = loginWithDeviceCodeFlow(ctx, dockerCli)
184+
msg, err = loginWithDeviceCodeFlow(ctx, dockerCLI)
174185
// if the error represents a failure to initiate the device-code flow,
175186
// then we fallback to regular cli credentials login
176187
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
177188
return msg, err
178189
}
179-
_, _ = fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
190+
_, _ = fmt.Fprint(dockerCLI.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
180191
}
181192

182-
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
193+
return loginWithUsernameAndPassword(ctx, dockerCLI, opts, defaultUsername, serverAddress)
183194
}
184195

185-
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
196+
func loginWithUsernameAndPassword(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
186197
// Prompt user for credentials
187-
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
198+
authConfig, err := command.PromptUserForCredentials(ctx, dockerCLI, opts.user, opts.password, defaultUsername, serverAddress)
188199
if err != nil {
189200
return "", err
190201
}
191202

192-
response, err := loginWithRegistry(ctx, dockerCli.Client(), authConfig)
203+
response, err := loginWithRegistry(ctx, dockerCLI.Client(), authConfig)
193204
if err != nil {
194205
return "", err
195206
}
@@ -198,26 +209,26 @@ func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, op
198209
authConfig.Password = ""
199210
authConfig.IdentityToken = response.IdentityToken
200211
}
201-
if err = storeCredentials(dockerCli.ConfigFile(), authConfig); err != nil {
212+
if err = storeCredentials(dockerCLI.ConfigFile(), authConfig); err != nil {
202213
return "", err
203214
}
204215

205216
return response.Status, nil
206217
}
207218

208-
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (msg string, _ error) {
209-
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
210-
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
219+
func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg string, _ error) {
220+
store := dockerCLI.ConfigFile().GetCredentialsStore(registry.IndexServer)
221+
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCLI.Err())
211222
if err != nil {
212223
return "", err
213224
}
214225

215-
response, err := loginWithRegistry(ctx, dockerCli.Client(), registrytypes.AuthConfig(*authConfig))
226+
response, err := loginWithRegistry(ctx, dockerCLI.Client(), registrytypes.AuthConfig(*authConfig))
216227
if err != nil {
217228
return "", err
218229
}
219230

220-
if err = storeCredentials(dockerCli.ConfigFile(), registrytypes.AuthConfig(*authConfig)); err != nil {
231+
if err = storeCredentials(dockerCLI.ConfigFile(), registrytypes.AuthConfig(*authConfig)); err != nil {
221232
return "", err
222233
}
223234

cli/command/registry/login_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,12 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
6161
}{
6262
{
6363
inputAuthConfig: registrytypes.AuthConfig{},
64-
expectedMsg: "Authenticating with existing credentials...\n",
6564
},
6665
{
6766
inputAuthConfig: registrytypes.AuthConfig{
6867
Username: unknownUser,
6968
},
7069
expectedErr: errUnknownUser,
71-
expectedMsg: "Authenticating with existing credentials...\n",
7270
expectedErrMsg: fmt.Sprintf("Login did not succeed, error: %s\n", errUnknownUser),
7371
},
7472
}
@@ -83,7 +81,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
8381
assert.NilError(t, err)
8482
}
8583
assert.Check(t, is.Equal(tc.expectedMsg, cli.OutBuffer().String()))
86-
assert.Check(t, is.Equal(tc.expectedErrMsg, cli.ErrBuffer().String()))
84+
assert.Check(t, is.Contains(cli.ErrBuffer().String(), tc.expectedErrMsg))
8785
cli.ErrBuffer().Reset()
8886
cli.OutBuffer().Reset()
8987
}

cli/internal/oauth/manager/manager.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/docker/cli/cli/config/types"
1414
"github.com/docker/cli/cli/internal/oauth"
1515
"github.com/docker/cli/cli/internal/oauth/api"
16+
"github.com/docker/cli/cli/streams"
17+
"github.com/docker/cli/internal/tui"
1618
"github.com/docker/docker/registry"
1719
"github.com/morikuni/aec"
1820
"github.com/sirupsen/logrus"
@@ -93,7 +95,15 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut
9395
}
9496

9597
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB-BASED LOGIN"))
96-
_, _ = fmt.Fprintln(w, "To sign in with credentials on the command line, use 'docker login -u <username>'")
98+
99+
var out tui.Output
100+
switch stream := w.(type) {
101+
case *streams.Out:
102+
out = tui.NewOutput(stream)
103+
default:
104+
out = tui.NewOutput(streams.NewOut(w))
105+
}
106+
out.PrintNote("To sign in with credentials on the command line, use 'docker login -u <username>'\n")
97107
_, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode)
98108
_, _ = 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])
99109

docs/reference/commandline/login.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ Defaults to Docker Hub if no server is specified.
66

77
### Options
88

9-
| Name | Type | Default | Description |
10-
|:---------------------------------------------|:---------|:--------|:-----------------------------|
11-
| `-p`, `--password` | `string` | | Password |
12-
| [`--password-stdin`](#password-stdin) | `bool` | | Take the password from stdin |
13-
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
9+
| Name | Type | Default | Description |
10+
|:---------------------------------------------|:---------|:--------|:------------------------------------------------------------|
11+
| `-p`, `--password` | `string` | | Password or Personal Access Token (PAT) |
12+
| [`--password-stdin`](#password-stdin) | `bool` | | Take the Password or Personal Access Token (PAT) from stdin |
13+
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
1414

1515

1616
<!---MARKER_GEN_END-->

0 commit comments

Comments
 (0)