diff --git a/src/go/rpk/pkg/cli/cloud/byoc/install.go b/src/go/rpk/pkg/cli/cloud/byoc/install.go index d36f208b4897..34223f56fae4 100644 --- a/src/go/rpk/pkg/cli/cloud/byoc/install.go +++ b/src/go/rpk/pkg/cli/cloud/byoc/install.go @@ -73,9 +73,9 @@ func loginAndEnsurePluginVersion(ctx context.Context, fs afero.Fs, cfg *config.C if overrides.CloudToken != "" { token = overrides.CloudToken } else { - token, err = oauth.LoadFlow(ctx, fs, cfg, auth0.NewClient(overrides)) + token, err = oauth.LoadFlow(ctx, fs, cfg, auth0.NewClient(overrides), false) if err != nil { - return "", "", false, fmt.Errorf("unable to load the cloud token: %w", err) + return "", "", false, fmt.Errorf("unable to load the cloud token: %w. You may need to logout with 'rpk cloud logout --clear-credentials' and try again", err) } } diff --git a/src/go/rpk/pkg/cli/cloud/login.go b/src/go/rpk/pkg/cli/cloud/login.go index cc9401aa7a47..cb6dfc1614e7 100644 --- a/src/go/rpk/pkg/cli/cloud/login.go +++ b/src/go/rpk/pkg/cli/cloud/login.go @@ -29,7 +29,7 @@ import ( ) func newLoginCommand(fs afero.Fs, p *config.Params) *cobra.Command { - var save, noProfile bool + var save, noProfile, noBrowser bool cmd := &cobra.Command{ Use: "login", Short: "Log in to the Redpanda cloud", @@ -49,6 +49,8 @@ This will automatically launch your default web browser and prompt you to authenticate via our Redpanda Cloud page. Once you have successfully authenticated, you will be ready to use rpk cloud commands. +You may opt out of auto-opening the browser by passing the '--no-browser' flag. + CLIENT CREDENTIALS Cloud client credentials can be used to login to Redpanda, they can be created @@ -82,12 +84,14 @@ want to disable automatic profile creation and selection, use --no-profile. if auth != nil { cc = auth.HasClientCredentials() } - _, err = oauth.LoadFlow(cmd.Context(), fs, cfg, auth0.NewClient(cfg.DevOverrides())) + _, err = oauth.LoadFlow(cmd.Context(), fs, cfg, auth0.NewClient(cfg.DevOverrides()), noBrowser) if err != nil { fmt.Printf("Unable to login to Redpanda Cloud (%v).\n", err) if e := (*oauth.BadClientTokenError)(nil); errors.As(err, &e) && cc { fmt.Println(`You may need to clear your client ID and secret with 'rpk cloud logout --clear-credentials', and then re-specify the client credentials next time you log in.`) + } else { + fmt.Println(`You may need to clear your credentials with 'rpk cloud logout --clear-credentials', and login again`) } os.Exit(1) } @@ -116,6 +120,7 @@ and then re-specify the client credentials next time you log in.`) p.InstallCloudFlags(cmd) cmd.Flags().BoolVar(&noProfile, "no-profile", false, "Skip automatic profile creation and any associated prompts") + cmd.Flags().BoolVar(&noBrowser, "no-browser", false, "Opt out of auto-opening authentication URL") cmd.Flags().BoolVar(&save, "save", false, "Save environment or flag specified client ID and client secret to the configuration file") return cmd } diff --git a/src/go/rpk/pkg/oauth/load.go b/src/go/rpk/pkg/oauth/load.go index 717d91eebbd8..fc6f85094e08 100644 --- a/src/go/rpk/pkg/oauth/load.go +++ b/src/go/rpk/pkg/oauth/load.go @@ -14,7 +14,7 @@ import ( // // This function is expected to be called at the start of most commands, and it // saves the token and client ID to the passed cloud config. -func LoadFlow(ctx context.Context, fs afero.Fs, cfg *config.Config, cl Client) (token string, err error) { +func LoadFlow(ctx context.Context, fs afero.Fs, cfg *config.Config, cl Client, noUI bool) (token string, err error) { // We want to avoid creating a root owned file. If the file exists, we // just chmod with rpkos.ReplaceFile and keep old perms even with sudo. // If the file does not exist, we will always be creating it to write @@ -30,7 +30,7 @@ func LoadFlow(ctx context.Context, fs afero.Fs, cfg *config.Config, cl Client) ( if authVir.HasClientCredentials() { resp, err = ClientCredentialFlow(ctx, cl, authVir) } else { - resp, err = DeviceFlow(ctx, cl, authVir) + resp, err = DeviceFlow(ctx, cl, authVir, noUI) } if err != nil { return "", fmt.Errorf("unable to retrieve a cloud token: %w", err) @@ -39,7 +39,7 @@ func LoadFlow(ctx context.Context, fs afero.Fs, cfg *config.Config, cl Client) ( // We want to update the actual auth. yAct, err := cfg.ActualRpkYamlOrEmpty() if err != nil { - return "", err + return "", fmt.Errorf("unable to load your rpk.yaml file: %v", err) } authAct := yAct.Auth(yAct.CurrentCloudAuth) if authAct == nil { diff --git a/src/go/rpk/pkg/oauth/load_test.go b/src/go/rpk/pkg/oauth/load_test.go index 5817097923f2..4b11f1d4d22b 100644 --- a/src/go/rpk/pkg/oauth/load_test.go +++ b/src/go/rpk/pkg/oauth/load_test.go @@ -87,7 +87,7 @@ func TestLoadFlow(t *testing.T) { } cfg, err := p.Load(fs) require.NoError(t, err) - gotToken, err := LoadFlow(context.Background(), fs, cfg, &m) + gotToken, err := LoadFlow(context.Background(), fs, cfg, &m, false) if tt.expErr { require.Error(t, err) return diff --git a/src/go/rpk/pkg/oauth/oauth.go b/src/go/rpk/pkg/oauth/oauth.go index feb3caba8a2f..1eebf03dd46e 100644 --- a/src/go/rpk/pkg/oauth/oauth.go +++ b/src/go/rpk/pkg/oauth/oauth.go @@ -64,7 +64,7 @@ func ClientCredentialFlow(ctx context.Context, cl Client, auth *config.RpkCloudA if auth.AuthToken != "" && auth.ClientID != "" { expired, err := ValidateToken(auth.AuthToken, cl.Audience(), auth.ClientID) if err != nil { - return Token{}, err + return Token{}, fmt.Errorf("unable to validate your authorization token: %v", err) } if !expired { return Token{AccessToken: auth.AuthToken}, nil @@ -75,7 +75,7 @@ func ClientCredentialFlow(ctx context.Context, cl Client, auth *config.RpkCloudA // DeviceFlow follows the OAuth 2.0 device authentication flow. First it // validates whether the configuration already have a valid token. -func DeviceFlow(ctx context.Context, cl Client, auth *config.RpkCloudAuth) (Token, error) { +func DeviceFlow(ctx context.Context, cl Client, auth *config.RpkCloudAuth, noUI bool) (Token, error) { // We only validate the token if we have the client ID, if one of them is // not present we just start the login flow again. if auth.AuthToken != "" && auth.ClientID != "" { @@ -95,12 +95,16 @@ func DeviceFlow(ctx context.Context, cl Client, auth *config.RpkCloudAuth) (Toke if !isURL(dcode.VerificationURLComplete) { return Token{}, fmt.Errorf("authorization server returned an invalid URL: %s; please contact Redpanda support", dcode.VerificationURLComplete) } - err = cl.URLOpener(dcode.VerificationURLComplete) - if err != nil { - return Token{}, fmt.Errorf("unable to open the web browser: %v", err) - } - fmt.Printf("Opening your browser for authentication, if does not open automatically, please open %q and proceed to login.\n", dcode.VerificationURLComplete) + if noUI { + fmt.Printf("For authentication, go to %q and log in.\n", dcode.VerificationURLComplete) + } else { + fmt.Printf("Opening your browser for authentication, if does not open automatically, please open %q and proceed to login.\n", dcode.VerificationURLComplete) + err = cl.URLOpener(dcode.VerificationURLComplete) + if err != nil { + return Token{}, fmt.Errorf("unable to open the web browser: %v; you may login using 'rpk cloud login --no-browser'", err) + } + } token, err := waitForDeviceToken(ctx, cl, dcode) if err != nil { diff --git a/src/go/rpk/pkg/oauth/oauth_test.go b/src/go/rpk/pkg/oauth/oauth_test.go index 0599e6c90b01..c12f8836f61e 100644 --- a/src/go/rpk/pkg/oauth/oauth_test.go +++ b/src/go/rpk/pkg/oauth/oauth_test.go @@ -242,7 +242,7 @@ func TestDeviceFlow(t *testing.T) { mockDeviceToken: tt.mDevToken, mockDevice: tt.mDevice, } - got, err := DeviceFlow(context.Background(), &cl, tt.auth) + got, err := DeviceFlow(context.Background(), &cl, tt.auth, false) if tt.expErr { require.Error(t, err) return