github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runbits/auth/login.go (about) 1 package auth 2 3 import ( 4 "net/url" 5 "time" 6 7 anaConst "github.com/ActiveState/cli/internal/analytics/constants" 8 "github.com/ActiveState/cli/internal/constants" 9 "github.com/ActiveState/cli/internal/errs" 10 "github.com/ActiveState/cli/internal/keypairs" 11 "github.com/ActiveState/cli/internal/locale" 12 "github.com/ActiveState/cli/internal/logging" 13 "github.com/ActiveState/cli/internal/osutils" 14 "github.com/ActiveState/cli/internal/output" 15 "github.com/ActiveState/cli/internal/prompt" 16 "github.com/ActiveState/cli/internal/rtutils/ptr" 17 "github.com/ActiveState/cli/pkg/platform/api" 18 "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" 19 secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets" 20 "github.com/ActiveState/cli/pkg/platform/authentication" 21 model "github.com/ActiveState/cli/pkg/platform/model/auth" 22 "github.com/go-openapi/strfmt" 23 ) 24 25 // OpenURI aliases to osutils.OpenURI which opens the given URI in your browser. This is being exposed so that it can be 26 // overwritten in tests 27 var OpenURI = osutils.OpenURI 28 29 // Authenticate will prompt the user for authentication 30 func Authenticate(cfg keypairs.Configurable, out output.Outputer, prompt prompt.Prompter, auth *authentication.Auth) error { 31 return AuthenticateWithInput("", "", "", false, cfg, out, prompt, auth) 32 } 33 34 // AuthenticateWithInput will prompt the user for authentication if the input doesn't already provide it 35 func AuthenticateWithInput( 36 username, password, totp string, 37 nonInteractive bool, 38 cfg keypairs.Configurable, 39 out output.Outputer, 40 prompt prompt.Prompter, 41 auth *authentication.Auth, 42 ) error { 43 logging.Debug("Authenticating with input") 44 45 credentials := &mono_models.Credentials{Username: username, Password: password, Totp: totp} 46 if err := ensureCredentials(credentials, prompt, nonInteractive); err != nil { 47 return locale.WrapInputError(err, "login_cancelled") 48 } 49 50 err := AuthenticateWithCredentials(credentials, auth) 51 if err != nil { 52 switch { 53 case errs.Matches(err, &authentication.ErrTokenRequired{}): 54 if err := promptToken(credentials, out, prompt, auth); err != nil { 55 return errs.Wrap(err, "promptToken failed") 56 } 57 case errs.Matches(err, &authentication.ErrUnauthorized{}): 58 return locale.WrapError(err, "err_auth_failed") 59 default: 60 return locale.WrapError(err, "err_auth_failed_unknown_cause", "", err.Error()) 61 } 62 } 63 64 if auth.Authenticated() { 65 secretsapi.InitializeClient(auth) 66 if err := ensureUserKeypair(credentials.Password, cfg, out, prompt, auth); err != nil { 67 return errs.Wrap(err, "ensureUserKeypair failed") 68 } 69 } 70 71 return nil 72 } 73 74 // AuthenticateWithToken will try to authenticate with the provided token 75 func AuthenticateWithToken(token string, auth *authentication.Auth) error { 76 logging.Debug("Authenticating with token") 77 78 if err := auth.AuthenticateWithModel(&mono_models.Credentials{ 79 Token: token, 80 }); err != nil { 81 return locale.WrapError(err, "err_auth_model", "Failed to authenticate.") 82 } 83 84 if err := auth.SaveToken(token); err != nil { 85 return locale.WrapError(err, "err_auth_token", "Failed to save token during token authentication.") 86 } 87 88 return nil 89 } 90 91 // RequireAuthentication will prompt the user for authentication if they are not already authenticated. If the authentication 92 // is not successful it will return a failure 93 func RequireAuthentication(message string, cfg keypairs.Configurable, out output.Outputer, prompt prompt.Prompter, auth *authentication.Auth) error { 94 if auth.Authenticated() { 95 return nil 96 } 97 98 out.Print(message) 99 100 choices := []string{ 101 locale.T("prompt_login_browser_action"), 102 locale.T("prompt_login_action"), 103 locale.T("prompt_signup_browser_action"), 104 } 105 choice, err := prompt.Select(locale.Tl("login_signup", "Login or Signup"), locale.T("prompt_login_or_signup"), choices, new(string)) 106 if err != nil { 107 return errs.Wrap(err, "Prompt cancelled") 108 } 109 110 switch choice { 111 case locale.T("prompt_login_browser_action"): 112 if err := AuthenticateWithBrowser(out, auth, prompt, cfg); err != nil { 113 return errs.Wrap(err, "Authenticate failed") 114 } 115 case locale.T("prompt_login_action"): 116 if err := Authenticate(cfg, out, prompt, auth); err != nil { 117 return errs.Wrap(err, "Authenticate failed") 118 } 119 case locale.T("prompt_signup_browser_action"): 120 if err := SignupWithBrowser(out, auth, prompt, cfg); err != nil { 121 return errs.Wrap(err, "Signup failed") 122 } 123 } 124 125 if !auth.Authenticated() { 126 return locale.NewInputError("err_auth_required") 127 } 128 129 return nil 130 } 131 132 func ensureCredentials(credentials *mono_models.Credentials, prompter prompt.Prompter, nonInteractive bool) error { 133 var err error 134 if credentials.Username == "" { 135 if nonInteractive { 136 return locale.NewInputError("err_auth_needinput") 137 } 138 credentials.Username, err = prompter.Input("", locale.T("username_prompt"), new(string), prompt.InputRequired) 139 if err != nil { 140 return errs.Wrap(err, "Input cancelled") 141 } 142 } 143 144 if credentials.Password == "" { 145 if nonInteractive { 146 return locale.NewInputError("err_auth_needinput") 147 } 148 credentials.Password, err = prompter.InputSecret("", locale.T("password_prompt"), prompt.InputRequired) 149 if err != nil { 150 return errs.Wrap(err, "Secret input cancelled") 151 } 152 } 153 return nil 154 } 155 156 // AuthenticateWithCredentials will attempt authenticate using the given credentials 157 func AuthenticateWithCredentials(credentials *mono_models.Credentials, auth *authentication.Auth) error { 158 logging.Debug("Authenticating with credentials") 159 160 err := auth.AuthenticateWithModel(credentials) 161 if err != nil { 162 return err 163 } 164 165 if err := auth.CreateToken(); err != nil { 166 return locale.WrapError(err, "err_auth_token", "Failed to create token while authenticating with credentials.") 167 } 168 169 return nil 170 } 171 172 func promptToken(credentials *mono_models.Credentials, out output.Outputer, prompt prompt.Prompter, auth *authentication.Auth) error { 173 var err error 174 credentials.Totp, err = prompt.Input("", locale.T("totp_prompt"), new(string)) 175 if err != nil { 176 return err 177 } 178 if credentials.Totp == "" { 179 out.Notice(locale.T("login_cancelled")) 180 return locale.NewInputError("err_auth_empty_token") 181 } 182 183 err = AuthenticateWithCredentials(credentials, auth) 184 if err != nil { 185 return err 186 } 187 188 return nil 189 } 190 191 // AuthenticateWithBrowser attempts to authenticate this device with the Platform. 192 func AuthenticateWithBrowser(out output.Outputer, auth *authentication.Auth, prompt prompt.Prompter, cfg keypairs.Configurable) error { 193 logging.Debug("Authenticating with browser") 194 195 err := authenticateWithBrowser(out, auth, prompt, cfg, false) 196 if err != nil { 197 return errs.Wrap(err, "Error authenticating with browser") 198 } 199 200 out.Notice(locale.T("auth_device_success")) 201 202 return nil 203 } 204 205 // authenticateWithBrowser authenticates after signup if applicable. 206 func authenticateWithBrowser(out output.Outputer, auth *authentication.Auth, prompt prompt.Prompter, cfg keypairs.Configurable, signup bool) error { 207 response, err := model.RequestDeviceAuthorization() 208 if err != nil { 209 return locale.WrapError(err, "err_auth_device") 210 } 211 212 if response.VerificationURIComplete == nil { 213 return errs.New("Invalid response: Missing verification URL.") 214 } 215 216 verificationURL := *response.VerificationURIComplete 217 parsedURL, err := url.Parse(verificationURL) 218 if err != nil { 219 return errs.Wrap(err, "Verification URL is not valid") 220 } 221 if signup { 222 // verificationURL is of the form: 223 // https://platform.activestate.com/authorize/device?user-code=... 224 // Transform it to the form: 225 // https://platform.activestate.com/create-account?nextRoute=%2Fauthorize%2Fdevice%3Fuser-code%3D... 226 signupURL := api.GetPlatformURL(constants.PlatformSignupPath) 227 query := signupURL.Query() 228 query.Add("nextRoute", parsedURL.RequestURI()) 229 signupURL.RawQuery = query.Encode() 230 parsedURL = signupURL 231 } 232 if webclientId := cfg.GetString(anaConst.CfgSessionToken); webclientId != "" { 233 query := parsedURL.Query() 234 query.Add("webclient_id", webclientId) 235 parsedURL.RawQuery = query.Encode() 236 } 237 verificationURL = parsedURL.String() 238 239 // Print code to user 240 if response.UserCode == nil { 241 return errs.New("Invalid response: Missing user code.") 242 } 243 out.Notice(locale.Tr("auth_device_verify_security_code", *response.UserCode, verificationURL)) 244 245 // Open URL in browser 246 err = OpenURI(verificationURL) 247 if err != nil { 248 logging.Warning("Could not open browser: %v", err) 249 out.Notice(locale.Tr("err_browser_open")) 250 } 251 252 var apiKey string 253 if !response.Nopoll { 254 // Wait for user to complete authentication 255 apiKey, err = auth.AuthenticateWithDevicePolling(strfmt.UUID(*response.DeviceCode), time.Duration(response.Interval)*time.Second) 256 if err != nil { 257 return locale.WrapError(err, "err_auth_device") 258 } 259 } else { 260 // This is the non-default behavior. If Nopoll = true we fall back on prompting the user to continue. It is a 261 // failsafe we can use in case polling overloads our API. 262 var cont bool 263 var err error 264 for !cont { 265 cont, err = prompt.Confirm(locale.Tl("continue", "Continue?"), locale.T("auth_press_enter"), ptr.To(false)) 266 if err != nil { 267 return errs.Wrap(err, "Prompt failed") 268 } 269 } 270 apiKey, err = auth.AuthenticateWithDevice(strfmt.UUID(*response.DeviceCode)) 271 if err != nil { 272 return locale.WrapError(err, "err_auth_device") 273 } 274 } 275 276 if err := auth.SaveToken(apiKey); err != nil { 277 return locale.WrapError(err, "err_auth_token", "Failed to create token after authenticating with browser.") 278 } 279 280 return nil 281 }