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 }