package shared import ( "bytes" "encoding/json" "fmt" "net/http" "os" "slices" "strings" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/authflow" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/pkg/cmd/ssh-key/add" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/ssh" ) const defaultSSHKeyTitle = "GitHub CLI" type iconfig interface { Login(string, string, string, string, bool) (bool, error) UsersForHost(string) []string } type LoginOptions struct { IO *iostreams.IOStreams Config iconfig HTTPClient *http.Client Hostname string Interactive bool Web bool Scopes []string GitProtocol string Prompter Prompt Browser browser.Browser CredentialFlow *GitCredentialFlow SecureStorage bool SkipSSHKeyPrompt bool CopyToClipboard bool sshContext ssh.Context } func Login(opts *LoginOptions) error { cfg := opts.Config hostname := opts.Hostname httpClient := opts.HTTPClient cs := opts.IO.ColorScheme() gitProtocol := strings.ToLower(opts.GitProtocol) if opts.Interactive && gitProtocol == "" { options := []string{ "HTTPS", "SSH", } result, err := opts.Prompter.Select( "What is your preferred protocol for Git operations on this host?", options[0], options) if err != nil { return err } proto := options[result] gitProtocol = strings.ToLower(proto) } var additionalScopes []string if opts.Interactive && gitProtocol == "https" { if err := opts.CredentialFlow.Prompt(hostname); err != nil { return err } additionalScopes = append(additionalScopes, opts.CredentialFlow.Scopes()...) } var keyToUpload string keyTitle := defaultSSHKeyTitle if opts.Interactive && !opts.SkipSSHKeyPrompt && gitProtocol == "ssh" { pubKeys, err := opts.sshContext.LocalPublicKeys() if err != nil { return err } if len(pubKeys) > 0 { options := append(pubKeys, "Skip") keyChoice, err := opts.Prompter.Select( "Upload your SSH public key to your GitHub account?", options[0], options) if err != nil { return err } if keyChoice < len(pubKeys) { keyToUpload = pubKeys[keyChoice] } } else if opts.sshContext.HasKeygen() { sshChoice, err := opts.Prompter.Confirm("Generate a new SSH key to add to your GitHub account?", true) if err != nil { return err } if sshChoice { passphrase, err := opts.Prompter.Password( "Enter a passphrase for your new SSH key (Optional):") if err != nil { return err } keyPair, err := opts.sshContext.GenerateSSHKey("id_ed25519", passphrase) if err != nil { return err } keyToUpload = keyPair.PublicKeyPath } } if keyToUpload != "" { var err error keyTitle, err = opts.Prompter.Input( "Title for your SSH key:", defaultSSHKeyTitle) if err != nil { return err } additionalScopes = append(additionalScopes, "admin:public_key") } } var authMode int if opts.Web { authMode = 0 } else if opts.Interactive { options := []string{"Login with a web browser", "Paste an authentication token"} var err error authMode, err = opts.Prompter.Select( "How would you like to authenticate GitHub CLI?", options[0], options) if err != nil { return err } } var authToken string var username string if authMode == 0 { var err error authToken, username, err = authflow.AuthFlow(hostname, opts.IO, "", append(opts.Scopes, additionalScopes...), opts.Interactive, opts.Browser, opts.CopyToClipboard) if err != nil { return fmt.Errorf("failed to authenticate via web browser: %w", err) } fmt.Fprintf(opts.IO.ErrOut, "%s Authentication complete.\n", cs.SuccessIcon()) } 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))) var err error authToken, err = opts.Prompter.AuthToken() if err != nil { return err } if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil { return fmt.Errorf("error validating token: %w", err) } } if username == "" { var err error username, err = GetCurrentLogin(httpClient, hostname, authToken) if err != nil { return fmt.Errorf("error retrieving current user: %w", err) } } // Get these users before adding the new one, so that we can // check whether the user was already logged in later. // // In this case we ignore the error if the host doesn't exist // because that can occur when the user is logging into a host // for the first time. usersForHost := cfg.UsersForHost(hostname) userWasAlreadyLoggedIn := slices.Contains(usersForHost, username) if gitProtocol != "" { fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon()) } insecureStorageUsed, err := cfg.Login(hostname, username, authToken, gitProtocol, opts.SecureStorage) if err != nil { return err } if insecureStorageUsed { fmt.Fprintf(opts.IO.ErrOut, "%s Authentication credentials saved in plain text\n", cs.Yellow("!")) } if opts.CredentialFlow.ShouldSetup() { err := opts.CredentialFlow.Setup(hostname, username, authToken) if err != nil { return err } } if keyToUpload != "" { uploaded, err := sshKeyUpload(httpClient, hostname, keyToUpload, keyTitle) if err != nil { return err } if uploaded { fmt.Fprintf(opts.IO.ErrOut, "%s Uploaded the SSH key to your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload)) } else { fmt.Fprintf(opts.IO.ErrOut, "%s SSH key already existed on 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)) if userWasAlreadyLoggedIn { fmt.Fprintf(opts.IO.ErrOut, "%s You were already logged in to this account\n", cs.WarningIcon()) } return nil } func scopesSentence(scopes []string) string { quoted := make([]string, len(scopes)) for i, s := range scopes { quoted[i] = fmt.Sprintf("'%s'", s) } return strings.Join(quoted, ", ") } func sshKeyUpload(httpClient *http.Client, hostname, keyFile string, title string) (bool, error) { f, err := os.Open(keyFile) if err != nil { return false, err } defer f.Close() return add.SSHKeyUpload(httpClient, hostname, f, title) } func GetCurrentLogin(httpClient httpClient, hostname, authToken string) (string, error) { query := `query UserCurrent{viewer{login}}` reqBody, err := json.Marshal(map[string]interface{}{"query": query}) if err != nil { return "", err } result := struct { Data struct{ Viewer struct{ Login string } } }{} apiEndpoint := ghinstance.GraphQLEndpoint(hostname) req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(reqBody)) if err != nil { return "", err } req.Header.Set("Authorization", "token "+authToken) res, err := httpClient.Do(req) if err != nil { return "", err } defer res.Body.Close() if res.StatusCode > 299 { return "", api.HandleHTTPError(res) } decoder := json.NewDecoder(res.Body) err = decoder.Decode(&result) if err != nil { return "", err } return result.Data.Viewer.Login, nil }