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