github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/pkg/cmd/auth/shared/login_flow.go (about)

     1  package shared
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"strings"
     7  
     8  	"github.com/AlecAivazis/survey/v2"
     9  	"github.com/MakeNowJust/heredoc"
    10  	"github.com/andrewhsu/cli/v2/api"
    11  	"github.com/andrewhsu/cli/v2/internal/authflow"
    12  	"github.com/andrewhsu/cli/v2/internal/ghinstance"
    13  	"github.com/andrewhsu/cli/v2/pkg/iostreams"
    14  	"github.com/andrewhsu/cli/v2/pkg/prompt"
    15  )
    16  
    17  type iconfig interface {
    18  	Get(string, string) (string, error)
    19  	Set(string, string, string) error
    20  	Write() error
    21  }
    22  
    23  type LoginOptions struct {
    24  	IO          *iostreams.IOStreams
    25  	Config      iconfig
    26  	HTTPClient  *http.Client
    27  	Hostname    string
    28  	Interactive bool
    29  	Web         bool
    30  	Scopes      []string
    31  	Executable  string
    32  
    33  	sshContext sshContext
    34  }
    35  
    36  func Login(opts *LoginOptions) error {
    37  	cfg := opts.Config
    38  	hostname := opts.Hostname
    39  	httpClient := opts.HTTPClient
    40  	cs := opts.IO.ColorScheme()
    41  
    42  	var gitProtocol string
    43  	if opts.Interactive {
    44  		var proto string
    45  		err := prompt.SurveyAskOne(&survey.Select{
    46  			Message: "What is your preferred protocol for Git operations?",
    47  			Options: []string{
    48  				"HTTPS",
    49  				"SSH",
    50  			},
    51  		}, &proto)
    52  		if err != nil {
    53  			return fmt.Errorf("could not prompt: %w", err)
    54  		}
    55  		gitProtocol = strings.ToLower(proto)
    56  	}
    57  
    58  	var additionalScopes []string
    59  
    60  	credentialFlow := &GitCredentialFlow{Executable: opts.Executable}
    61  	if opts.Interactive && gitProtocol == "https" {
    62  		if err := credentialFlow.Prompt(hostname); err != nil {
    63  			return err
    64  		}
    65  		additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
    66  	}
    67  
    68  	var keyToUpload string
    69  	if opts.Interactive && gitProtocol == "ssh" {
    70  		pubKeys, err := opts.sshContext.localPublicKeys()
    71  		if err != nil {
    72  			return err
    73  		}
    74  
    75  		if len(pubKeys) > 0 {
    76  			var keyChoice int
    77  			err := prompt.SurveyAskOne(&survey.Select{
    78  				Message: "Upload your SSH public key to your GitHub account?",
    79  				Options: append(pubKeys, "Skip"),
    80  			}, &keyChoice)
    81  			if err != nil {
    82  				return fmt.Errorf("could not prompt: %w", err)
    83  			}
    84  			if keyChoice < len(pubKeys) {
    85  				keyToUpload = pubKeys[keyChoice]
    86  			}
    87  		} else {
    88  			var err error
    89  			keyToUpload, err = opts.sshContext.generateSSHKey()
    90  			if err != nil {
    91  				return err
    92  			}
    93  		}
    94  	}
    95  	if keyToUpload != "" {
    96  		additionalScopes = append(additionalScopes, "admin:public_key")
    97  	}
    98  
    99  	var authMode int
   100  	if opts.Web {
   101  		authMode = 0
   102  	} else {
   103  		err := prompt.SurveyAskOne(&survey.Select{
   104  			Message: "How would you like to authenticate GitHub CLI?",
   105  			Options: []string{
   106  				"Login with a web browser",
   107  				"Paste an authentication token",
   108  			},
   109  		}, &authMode)
   110  		if err != nil {
   111  			return fmt.Errorf("could not prompt: %w", err)
   112  		}
   113  	}
   114  
   115  	var authToken string
   116  	userValidated := false
   117  
   118  	if authMode == 0 {
   119  		var err error
   120  		authToken, err = authflow.AuthFlowWithConfig(cfg, opts.IO, hostname, "", append(opts.Scopes, additionalScopes...))
   121  		if err != nil {
   122  			return fmt.Errorf("failed to authenticate via web browser: %w", err)
   123  		}
   124  		userValidated = true
   125  	} else {
   126  		minimumScopes := append([]string{"repo", "read:org"}, additionalScopes...)
   127  		fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(`
   128  			Tip: you can generate a Personal Access Token here https://%s/settings/tokens
   129  			The minimum required scopes are %s.
   130  		`, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname))))
   131  
   132  		err := prompt.SurveyAskOne(&survey.Password{
   133  			Message: "Paste your authentication token:",
   134  		}, &authToken, survey.WithValidator(survey.Required))
   135  		if err != nil {
   136  			return fmt.Errorf("could not prompt: %w", err)
   137  		}
   138  
   139  		if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil {
   140  			return fmt.Errorf("error validating token: %w", err)
   141  		}
   142  
   143  		if err := cfg.Set(hostname, "oauth_token", authToken); err != nil {
   144  			return err
   145  		}
   146  	}
   147  
   148  	var username string
   149  	if userValidated {
   150  		username, _ = cfg.Get(hostname, "user")
   151  	} else {
   152  		apiClient := api.NewClientFromHTTP(httpClient)
   153  		var err error
   154  		username, err = api.CurrentLoginName(apiClient, hostname)
   155  		if err != nil {
   156  			return fmt.Errorf("error using api: %w", err)
   157  		}
   158  
   159  		err = cfg.Set(hostname, "user", username)
   160  		if err != nil {
   161  			return err
   162  		}
   163  	}
   164  
   165  	if gitProtocol != "" {
   166  		fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol)
   167  		err := cfg.Set(hostname, "git_protocol", gitProtocol)
   168  		if err != nil {
   169  			return err
   170  		}
   171  		fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon())
   172  	}
   173  
   174  	err := cfg.Write()
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	if credentialFlow.ShouldSetup() {
   180  		err := credentialFlow.Setup(hostname, username, authToken)
   181  		if err != nil {
   182  			return err
   183  		}
   184  	}
   185  
   186  	if keyToUpload != "" {
   187  		err := sshKeyUpload(httpClient, hostname, keyToUpload)
   188  		if err != nil {
   189  			return err
   190  		}
   191  		fmt.Fprintf(opts.IO.ErrOut, "%s Uploaded the SSH key to your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload))
   192  	}
   193  
   194  	fmt.Fprintf(opts.IO.ErrOut, "%s Logged in as %s\n", cs.SuccessIcon(), cs.Bold(username))
   195  	return nil
   196  }
   197  
   198  func scopesSentence(scopes []string, isEnterprise bool) string {
   199  	quoted := make([]string, len(scopes))
   200  	for i, s := range scopes {
   201  		quoted[i] = fmt.Sprintf("'%s'", s)
   202  		if s == "workflow" && isEnterprise {
   203  			// remove when GHE 2.x reaches EOL
   204  			quoted[i] += " (GHE 3.0+)"
   205  		}
   206  	}
   207  	return strings.Join(quoted, ", ")
   208  }