package shared import ( "fmt" "net/http" "strings" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/internal/authflow" "github.com/cli/cli/internal/ghinstance" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/prompt" ) type iconfig interface { Get(string, string) (string, error) Set(string, string, string) error Write() error } type LoginOptions struct { IO *iostreams.IOStreams Config iconfig HTTPClient *http.Client Hostname string Interactive bool Web bool Scopes []string Executable string sshContext sshContext } func Login(opts *LoginOptions) error { cfg := opts.Config hostname := opts.Hostname httpClient := opts.HTTPClient cs := opts.IO.ColorScheme() var gitProtocol string if opts.Interactive { var proto string err := prompt.SurveyAskOne(&survey.Select{ Message: "What is your preferred protocol for Git operations?", Options: []string{ "HTTPS", "SSH", }, }, &proto) if err != nil { return fmt.Errorf("could not prompt: %w", err) } gitProtocol = strings.ToLower(proto) } var additionalScopes []string credentialFlow := &GitCredentialFlow{Executable: opts.Executable} if opts.Interactive && gitProtocol == "https" { if err := credentialFlow.Prompt(hostname); err != nil { return err } additionalScopes = append(additionalScopes, credentialFlow.Scopes()...) } var keyToUpload string if opts.Interactive && gitProtocol == "ssh" { pubKeys, err := opts.sshContext.localPublicKeys() if err != nil { return err } if len(pubKeys) > 0 { var keyChoice int err := prompt.SurveyAskOne(&survey.Select{ Message: "Upload your SSH public key to your GitHub account?", Options: append(pubKeys, "Skip"), }, &keyChoice) if err != nil { return fmt.Errorf("could not prompt: %w", err) } if keyChoice < len(pubKeys) { keyToUpload = pubKeys[keyChoice] } } else { var err error keyToUpload, err = opts.sshContext.generateSSHKey() if err != nil { return err } } } if keyToUpload != "" { additionalScopes = append(additionalScopes, "admin:public_key") } var authMode int if opts.Web { authMode = 0 } else { err := prompt.SurveyAskOne(&survey.Select{ Message: "How would you like to authenticate GitHub CLI?", Options: []string{ "Login with a web browser", "Paste an authentication token", }, }, &authMode) if err != nil { return fmt.Errorf("could not prompt: %w", err) } } var authToken string userValidated := false if authMode == 0 { var err error authToken, err = authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", append(opts.Scopes, additionalScopes...)) if err != nil { return fmt.Errorf("failed to authenticate via web browser: %w", err) } userValidated = true } else { minimumScopes := append([]string{"repo", "read:org"}, additionalScopes...) fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(` Tip: you can generate a Personal Access Token here https://%s/settings/tokens The minimum required scopes are %s. `, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname)))) err := prompt.SurveyAskOne(&survey.Password{ Message: "Paste your authentication token:", }, &authToken, survey.WithValidator(survey.Required)) if err != nil { return fmt.Errorf("could not prompt: %w", err) } if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil { return fmt.Errorf("error validating token: %w", err) } if err := cfg.Set(hostname, "oauth_token", authToken); err != nil { return err } } var username string if userValidated { username, _ = cfg.Get(hostname, "user") } else { apiClient := api.NewClientFromHTTP(httpClient) var err error username, err = api.CurrentLoginName(apiClient, hostname) if err != nil { return fmt.Errorf("error using api: %w", err) } err = cfg.Set(hostname, "user", username) if err != nil { return err } } if gitProtocol != "" { fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) err := cfg.Set(hostname, "git_protocol", gitProtocol) if err != nil { return err } fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon()) } err := cfg.Write() if err != nil { return err } if credentialFlow.ShouldSetup() { err := credentialFlow.Setup(hostname, username, authToken) if err != nil { return err } } if keyToUpload != "" { err := sshKeyUpload(httpClient, hostname, keyToUpload) if err != nil { return err } fmt.Fprintf(opts.IO.ErrOut, "%s Uploaded the SSH key to your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload)) } fmt.Fprintf(opts.IO.ErrOut, "%s Logged in as %s\n", cs.SuccessIcon(), cs.Bold(username)) return nil } func scopesSentence(scopes []string, isEnterprise bool) string { quoted := make([]string, len(scopes)) for i, s := range scopes { quoted[i] = fmt.Sprintf("'%s'", s) if s == "workflow" && isEnterprise { // remove when GHE 2.x reaches EOL quoted[i] += " (GHE 3.0+)" } } return strings.Join(quoted, ", ") }