github.com/henvic/wedeploycli@v1.7.6-0.20200319005353-3630f582f284/login/login.go (about)

     1  package login
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/url"
    11  	"os"
    12  	"runtime"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/henvic/browser"
    17  	"github.com/henvic/wedeploycli/color"
    18  	"github.com/henvic/wedeploycli/command/canceled"
    19  	"github.com/henvic/wedeploycli/config"
    20  	"github.com/henvic/wedeploycli/defaults"
    21  	"github.com/henvic/wedeploycli/fancy"
    22  	"github.com/henvic/wedeploycli/figures"
    23  	"github.com/henvic/wedeploycli/formatter"
    24  	"github.com/henvic/wedeploycli/isterm"
    25  	"github.com/henvic/wedeploycli/loginserver"
    26  	"github.com/henvic/wedeploycli/status"
    27  	"github.com/henvic/wedeploycli/timehelper"
    28  	"github.com/henvic/wedeploycli/usertoken"
    29  	"github.com/henvic/wedeploycli/verbose"
    30  	"github.com/henvic/wedeploycli/waitlivemsg"
    31  )
    32  
    33  func validateEmail(email string) (bool, error) {
    34  	if len(email) == 0 {
    35  		return false, errors.New("please enter your email")
    36  	}
    37  
    38  	var index = strings.Index(email, "@")
    39  
    40  	if index == -1 {
    41  		return false, errors.New(`please enter your full email address, including the "@"`)
    42  	}
    43  
    44  	if index+1 == len(email) {
    45  		return false, errors.New(`do not forget the part after the "@"`)
    46  	}
    47  
    48  	return true, nil
    49  }
    50  
    51  func validatePassword(password string) (bool, error) {
    52  	if len(password) == 0 {
    53  		return false, errors.New("please enter your password")
    54  	}
    55  
    56  	return true, nil
    57  }
    58  
    59  // Authentication service
    60  type Authentication struct {
    61  	NoLaunchBrowser bool
    62  	Domains         status.Domains
    63  	TipCommands     bool
    64  	wectx           config.Context
    65  	wlm             *waitlivemsg.WaitLiveMsg
    66  	msg             *waitlivemsg.Message
    67  }
    68  
    69  func (a *Authentication) basicAuthLogin(ctx context.Context) error {
    70  	var remoteAddress = a.wectx.InfrastructureDomain()
    71  
    72  	fmt.Println(fancy.Info("Alert     You need a Liferay Cloud password for authenticating without opening your browser." +
    73  		"\n          If you created your Liferay Cloud account by using OAuth," +
    74  		"\n          make sure you set up a password to continue."))
    75  	fmt.Println(color.Format(color.FgHiYellow, "\n            Open this URL in your browser for creating a password:"))
    76  	fmt.Println(color.Format(color.FgHiBlack, fmt.Sprintf("            %v%v/password/reset\n", defaults.DashboardURLPrefix, remoteAddress)))
    77  
    78  	fmt.Println(fancy.Question("Type your credentials for logging in. Your email: ") + color.Format(color.FgHiBlack, "[ex: user@domain.com]"))
    79  promptForUsername:
    80  
    81  	username, err := fancy.Prompt()
    82  
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	if validEmailAddress, invalidEmailAddressMsg := validateEmail(username); !validEmailAddress {
    88  		_, _ = fmt.Fprintf(os.Stderr, "%s\n", fancy.Error(invalidEmailAddressMsg))
    89  		goto promptForUsername
    90  	}
    91  
    92  	fmt.Print(fancy.Question("Great! Now, your password:\n"))
    93  promptForPassword:
    94  	password, err := fancy.HiddenPrompt()
    95  
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	fmt.Println(color.Format(color.FgHiBlack, "●●●●●●●●●●"))
   101  	if validPassword, invalidPasswordMsg := validatePassword(password); !validPassword {
   102  		_, _ = fmt.Fprintf(os.Stderr, "%s\n", fancy.Error(invalidPasswordMsg))
   103  		goto promptForPassword
   104  	}
   105  
   106  	return a.loginWithCredentials(ctx, username, password)
   107  }
   108  
   109  func (a *Authentication) loginWithCredentials(ctx context.Context, username, password string) error {
   110  	a.wlm = waitlivemsg.New(nil)
   111  	a.msg = waitlivemsg.NewMessage("Authenticating [1/2]")
   112  	a.wlm.AddMessage(a.msg)
   113  	go a.wlm.Wait()
   114  	defer a.wlm.Stop()
   115  
   116  	var ba = loginserver.BasicAuth{
   117  		Username: username,
   118  		Password: password,
   119  		Context:  a.wectx,
   120  	}
   121  
   122  	var token, err = ba.GetOAuthToken(ctx)
   123  	a.maybePrintReceivedToken(token)
   124  
   125  	if err != nil {
   126  		a.msg.StopText(fancy.Error("Authentication failed [1/2]"))
   127  		return err
   128  	}
   129  
   130  	return a.saveUser(username, token)
   131  }
   132  
   133  func (a *Authentication) tryStdinToken() (bool, error) {
   134  	// trade-off: --no-tty is required for piping tokens on some Windows """shell subsystems"""
   135  	// see issue https://github.com/wedeploy/cli/issues/435
   136  	// error first appeared in commit 4d217d2324825714bf6fa35d988502692f0d7925
   137  	if runtime.GOOS == "windows" &&
   138  		(os.Getenv("OSTYPE") == "cygwin" ||
   139  			strings.Contains(os.Getenv("MSYSTEM_CHOST"), "mingw") ||
   140  			strings.Contains(os.Getenv("MINGW_CHOST"), "mingw")) {
   141  		verbose.Debug("INFO: --no-tty is required to pipe credentials values such as tokens using STDIN on some Windows environments")
   142  
   143  		if !isterm.NoTTY {
   144  			return false, nil
   145  		}
   146  	}
   147  
   148  	file := os.Stdin
   149  	fi, err := file.Stat()
   150  
   151  	// Different systems treat Stdin differently
   152  	// On Ubuntu (Linux), the stdin size is zero even if all
   153  	// content was already piped, say with:
   154  	// echo foo | lcp login
   155  	// On Darwin (macOS), this is not the case.
   156  	// See http://learngowith.me/a-better-way-of-handling-stdin/
   157  
   158  	if fi.Size() != 0 {
   159  		goto skipToStdin
   160  	}
   161  
   162  	if err != nil || fi.Mode()&os.ModeCharDevice != 0 {
   163  		return false, nil
   164  	}
   165  
   166  skipToStdin:
   167  	reader := bufio.NewReader(file)
   168  	maybe, err := reader.ReadString('\n')
   169  
   170  	if err != nil && err != io.EOF {
   171  		return false, nil
   172  	}
   173  
   174  	maybe = strings.TrimSuffix(maybe, "\n")
   175  
   176  	if sep := strings.Index(maybe, " "); sep != -1 {
   177  		return true, a.loginWithCredentials(context.Background(), maybe[:sep], maybe[sep+1:])
   178  	}
   179  
   180  	return true, a.loginWithToken(maybe)
   181  }
   182  
   183  func (a *Authentication) loginWithToken(token string) error {
   184  	wt, err := usertoken.ParseUnsignedJSONWebToken(token)
   185  
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	a.wlm = waitlivemsg.New(nil)
   191  	a.msg = waitlivemsg.NewMessage("Authenticating [1/2]")
   192  	a.wlm.AddMessage(a.msg)
   193  	go a.wlm.Wait()
   194  	defer a.wlm.Stop()
   195  	return a.saveUser(wt.Email, token)
   196  }
   197  
   198  // Run authentication process
   199  func (a *Authentication) Run(ctx context.Context, c config.Context) error {
   200  	a.wectx = c
   201  	statusClient := status.New(c)
   202  
   203  	s, err := statusClient.UnsafeGet(ctx)
   204  
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	a.Domains = s.Domains
   210  
   211  	if stdin, stdinErr := a.tryStdinToken(); stdin {
   212  		return stdinErr
   213  	}
   214  
   215  	if a.NoLaunchBrowser {
   216  		return a.basicAuthLogin(ctx)
   217  	}
   218  
   219  	yes, err := fancy.Boolean("Open your browser and authenticate?")
   220  
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	if !yes {
   226  		return canceled.CancelCommand("login canceled")
   227  	}
   228  
   229  	return a.browserWorkflowAuth()
   230  }
   231  
   232  func (a *Authentication) maybeOpenBrowser(loginURL string) {
   233  	if verbose.Enabled {
   234  		a.wlm.AddMessage(waitlivemsg.NewMessage("Login URL: " + loginURL))
   235  	}
   236  
   237  	time.Sleep(710 * time.Millisecond)
   238  
   239  	if err := browser.OpenURL(loginURL); err != nil {
   240  		errMsg := &waitlivemsg.Message{}
   241  		errMsg.StopText(fmt.Sprintf("%v", err))
   242  		a.wlm.AddMessage(errMsg)
   243  
   244  		if !verbose.Enabled {
   245  			a.wlm.AddMessage(waitlivemsg.NewMessage("Open URL: (can't open automatically) " + loginURL))
   246  		}
   247  	}
   248  }
   249  
   250  func (a *Authentication) browserWorkflowAuth() error {
   251  	a.wlm = waitlivemsg.New(nil)
   252  	a.msg = waitlivemsg.NewMessage("Waiting for authentication via browser [1/2]\n" +
   253  		fancy.Tip("^C to cancel"))
   254  	a.wlm.AddMessage(a.msg)
   255  	go a.wlm.Wait()
   256  	defer a.wlm.Stop()
   257  	var service = &loginserver.Service{
   258  		Infrastructure: a.Domains.Infrastructure,
   259  	}
   260  	var host, err = service.Listen(context.Background())
   261  
   262  	if err != nil {
   263  		a.msg.StopText(fancy.Error("Authentication failed [1/2]"))
   264  		return err
   265  	}
   266  
   267  	var loginURL = fmt.Sprintf("%s%s%s%s",
   268  		defaults.DashboardURLPrefix,
   269  		a.wectx.InfrastructureDomain(),
   270  		"/login?redirect_uri=",
   271  		url.QueryEscape(host))
   272  
   273  	a.maybeOpenBrowser(loginURL)
   274  
   275  	if err = service.Serve(); err != nil {
   276  		a.msg.StopText(fancy.Error("Authentication failed [1/2]"))
   277  		return err
   278  	}
   279  
   280  	var username, token, tokenErr = service.Credentials()
   281  	a.maybePrintReceivedToken(token)
   282  
   283  	if tokenErr != nil {
   284  		a.msg.StopText(fancy.Error("Authentication failed [1/2]"))
   285  		return tokenErr
   286  	}
   287  
   288  	return a.saveUser(username, token)
   289  }
   290  
   291  func (a *Authentication) success(username string) {
   292  	var duration = a.wlm.Duration()
   293  	var conf = a.wectx.Config()
   294  	var params = conf.GetParams()
   295  	var rl = params.Remotes
   296  	var remote = rl.Get(a.wectx.Remote())
   297  
   298  	var buf = &bytes.Buffer{}
   299  	_, _ = fmt.Fprintf(buf, "%s Authentication completed in %s [2/2]\n", figures.Tick, timehelper.RoundDuration(duration, time.Second))
   300  	_, _ = fmt.Fprintf(buf, "You're logged in as \"%s\" on \"%s\".\n",
   301  		color.Format(color.Reset, color.Bold, username),
   302  		color.Format(color.Reset, color.Bold, remote.Infrastructure))
   303  
   304  	if a.TipCommands {
   305  		a.printTipCommands(buf)
   306  	}
   307  	a.msg.StopText(buf.String())
   308  }
   309  
   310  func (a *Authentication) printTipCommands(buf *bytes.Buffer) {
   311  	_, _ = fmt.Fprintln(buf, fancy.Info("See some of the useful commands you can start using on the Liferay Cloud Platform CLI.\n"))
   312  	tw := formatter.NewTabWriter(buf)
   313  	_, _ = fmt.Fprintln(tw, color.Format(color.FgHiBlack, "  Command\t     Description"))
   314  	_, _ = fmt.Fprintln(tw, "  lcp\tShow list of all commands available in Liferay Cloud Platform CLI")
   315  	_, _ = fmt.Fprintln(tw, "  lcp deploy\tDeploy your services")
   316  	_, _ = fmt.Fprintln(tw, "  lcp docs\tOpen docs on your browser")
   317  	_ = tw.Flush()
   318  	_, _ = fmt.Fprint(buf, fancy.Info("\nType a command and press Enter to execute it."))
   319  }
   320  
   321  func (a *Authentication) saveUser(username, token string) (err error) {
   322  	var conf = a.wectx.Config()
   323  	var params = conf.GetParams()
   324  	var rl = params.Remotes
   325  	var remote = rl.Get(a.wectx.Remote())
   326  	remote.Username = username
   327  	remote.Token = token
   328  	remote.Infrastructure = a.Domains.Infrastructure
   329  	remote.Service = a.Domains.Service
   330  
   331  	rl.Set(a.wectx.Remote(), remote)
   332  
   333  	if err = a.wectx.SetEndpoint(a.wectx.Remote()); err != nil {
   334  		a.msg.StopText(fancy.Error("Authentication failed [1/2]"))
   335  		return err
   336  	}
   337  
   338  	if err = conf.Save(); err != nil {
   339  		a.msg.StopText(fancy.Error("Authentication failed [1/2]"))
   340  		return err
   341  	}
   342  
   343  	a.success(username)
   344  	return nil
   345  }
   346  
   347  func (a *Authentication) maybePrintReceivedToken(token string) {
   348  	if verbose.Enabled {
   349  		tokenMsg := &waitlivemsg.Message{}
   350  		tokenMsg.StopText("Token: " + verbose.SafeEscape(token))
   351  		a.wlm.AddMessage(tokenMsg)
   352  	}
   353  }