Skip to content
Draft
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
58 changes: 20 additions & 38 deletions cli/command/registry/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,7 @@ func runLogin(ctx context.Context, dockerCLI command.Cli, opts loginOptions) err

maybePrintEnvAuthWarning(dockerCLI)

var (
serverAddress string
msg string
)
var serverAddress string
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
serverAddress = opts.serverAddress
} else {
Expand All @@ -132,25 +129,23 @@ func runLogin(ctx context.Context, dockerCLI command.Cli, opts loginOptions) err
// attempt login with current (stored) credentials
authConfig, err := command.GetDefaultAuthConfig(dockerCLI.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
msg, err = loginWithStoredCredentials(ctx, dockerCLI, authConfig)
err = loginWithStoredCredentials(ctx, dockerCLI, authConfig)
}

// if we failed to authenticate with stored credentials (or didn't have stored credentials),
// prompt the user for new credentials
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
msg, err = loginUser(ctx, dockerCLI, opts, authConfig.Username, authConfig.ServerAddress)
err = loginUser(ctx, dockerCLI, opts, authConfig.Username, authConfig.ServerAddress)
if err != nil {
return err
}
}

if msg != "" {
_, _ = fmt.Fprintln(dockerCLI.Out(), msg)
}
_, _ = fmt.Fprintln(dockerCLI.Out(), "Login Succeeded")
return nil
}

func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, authConfig registrytypes.AuthConfig) (msg string, _ error) {
func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, authConfig registrytypes.AuthConfig) error {
_, _ = fmt.Fprintf(dockerCLI.Err(), "Authenticating with existing credentials...")
if authConfig.Username != "" {
_, _ = fmt.Fprintf(dockerCLI.Err(), " [Username: %s]", authConfig.Username)
Expand All @@ -169,18 +164,15 @@ func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, auth
} else {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Login did not succeed, error:", err)
}
return err
}

if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}

if err := storeCredentials(dockerCLI.ConfigFile(), authConfig); err != nil {
return "", err
}

return response.Status, err
return storeCredentials(dockerCLI.ConfigFile(), authConfig)
Comment on lines -179 to +175
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! The existing code shadowed the error; so this function would return when failing to store the credentials, but the error returned is the error from earlier?? return response.Status, err returns the err from response, err := dockerCLI.Client().RegistryLogin(ctx, authConfig), but even on failure, it would still try to store the credentials 🤔

I suspect that was a bug!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may have been introduced in 6e4818e

Before that patch, loginWithCredStoreCreds would only try to login, but did not handle saving credentials.

func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
cliClient := dockerCli.Client()
response, err := cliClient.RegistryLogin(ctx, *authConfig)
if err != nil {
if errdefs.IsUnauthorized(err) {
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
} else {
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
}
}
return response, err
}

So question is; was it intentional to save credentials even if they were invalid or expired? Or was the intent perhaps to remove / reset those credentials so that they wouldn't be used again?

if err := storeCredentials(dockerCLI.ConfigFile(), authConfig); err != nil {
return "", err
}

}

// OauthLoginEscapeHatchEnvVar disables the browser-based OAuth login workflow.
Expand All @@ -201,7 +193,7 @@ func isOauthLoginDisabled() bool {
return false
}

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) error {
// Some links documenting this:
// - https://code.google.com/archive/p/mintty/issues/56
// - https://github.com/docker/docker/issues/15272
Expand All @@ -210,64 +202,55 @@ func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, de
// will hit this if you attempt docker login from mintty where stdin
// is a pipe, not a character based console.
if (opts.user == "" || opts.password == "") && !dockerCLI.In().IsTerminal() {
return "", errors.New("error: cannot perform an interactive login from a non TTY device")
return errors.New("error: cannot perform an interactive login from a non-TTY device")
}

// 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 == "" && !isOauthLoginDisabled() {
var err error
msg, err = loginWithDeviceCodeFlow(ctx, dockerCLI)
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) {
return msg, err
return err
}
_, _ = fmt.Fprint(dockerCLI.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
}

return loginWithUsernameAndPassword(ctx, dockerCLI, opts, defaultUsername, serverAddress)
}

func loginWithUsernameAndPassword(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
func loginWithUsernameAndPassword(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) error {
// Prompt user for credentials
authConfig, err := command.PromptUserForCredentials(ctx, dockerCLI, opts.user, opts.password, defaultUsername, serverAddress)
if err != nil {
return "", err
return err
}

response, err := loginWithRegistry(ctx, dockerCLI.Client(), authConfig)
if err != nil {
return "", err
return err
}

if response.IdentityToken != "" {
authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken
}
if err = storeCredentials(dockerCLI.ConfigFile(), authConfig); err != nil {
return "", err
}

return response.Status, nil
return storeCredentials(dockerCLI.ConfigFile(), authConfig)
}

func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg string, _ error) {
func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) error {
store := dockerCLI.ConfigFile().GetCredentialsStore(registry.IndexServer)
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCLI.Err())
if err != nil {
return "", err
return err
}

response, err := loginWithRegistry(ctx, dockerCLI.Client(), registrytypes.AuthConfig(*authConfig))
_, err = loginWithRegistry(ctx, dockerCLI.Client(), registrytypes.AuthConfig(*authConfig))
if err != nil {
return "", err
}

if err = storeCredentials(dockerCLI.ConfigFile(), registrytypes.AuthConfig(*authConfig)); err != nil {
return "", err
return err
}

return response.Status, nil
return storeCredentials(dockerCLI.ConfigFile(), registrytypes.AuthConfig(*authConfig))
}

func storeCredentials(cfg *configfile.ConfigFile, authConfig registrytypes.AuthConfig) error {
Expand Down Expand Up @@ -304,7 +287,6 @@ func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*regis
}

return &registrytypes.AuthenticateOKBody{
Status: "Login Succeeded",
IdentityToken: token,
}, nil
}
14 changes: 7 additions & 7 deletions cli/command/registry/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.ConfigFile().Filename = filepath.Join(t.TempDir(), "config.json")
for _, tc := range testCases {
_, err := loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
err := loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
if tc.expectedErrMsg != "" {
assert.Check(t, is.Error(err, tc.expectedErr))
} else {
Expand Down Expand Up @@ -128,7 +128,7 @@ func TestRunLogin(t *testing.T) {
input: loginOptions{
serverAddress: "reg1",
},
expectedErr: "error: cannot perform an interactive login from a non TTY device",
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
},
{
doc: "store valid username and password",
Expand Down Expand Up @@ -335,19 +335,19 @@ func TestLoginNonInteractive(t *testing.T) {
doc: "error - w/o user w/o pass ",
username: false,
password: false,
expectedErr: "error: cannot perform an interactive login from a non TTY device",
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
},
{
doc: "error - w/ user w/o pass",
username: true,
password: false,
expectedErr: "error: cannot perform an interactive login from a non TTY device",
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
},
{
doc: "error - w/o user w/ pass",
username: false,
password: true,
expectedErr: "error: cannot perform an interactive login from a non TTY device",
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
},
}

Expand Down Expand Up @@ -404,13 +404,13 @@ func TestLoginNonInteractive(t *testing.T) {
doc: "error - w/ user w/o pass",
username: true,
password: false,
expectedErr: "error: cannot perform an interactive login from a non TTY device",
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
},
{
doc: "error - w/o user w/ pass",
username: false,
password: true,
expectedErr: "error: cannot perform an interactive login from a non TTY device",
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
},
}

Expand Down
Loading