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 }