github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/auth/login/login.go (about)

     1  package login
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/MakeNowJust/heredoc"
    10  	"github.com/ungtb10d/cli/v2/git"
    11  	"github.com/ungtb10d/cli/v2/internal/config"
    12  	"github.com/ungtb10d/cli/v2/internal/ghinstance"
    13  	"github.com/ungtb10d/cli/v2/pkg/cmd/auth/shared"
    14  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    15  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    16  	ghAuth "github.com/cli/go-gh/pkg/auth"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  type LoginOptions struct {
    21  	IO         *iostreams.IOStreams
    22  	Config     func() (config.Config, error)
    23  	HttpClient func() (*http.Client, error)
    24  	GitClient  *git.Client
    25  	Prompter   shared.Prompt
    26  
    27  	MainExecutable string
    28  
    29  	Interactive bool
    30  
    31  	Hostname    string
    32  	Scopes      []string
    33  	Token       string
    34  	Web         bool
    35  	GitProtocol string
    36  }
    37  
    38  func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
    39  	opts := &LoginOptions{
    40  		IO:         f.IOStreams,
    41  		Config:     f.Config,
    42  		HttpClient: f.HttpClient,
    43  		GitClient:  f.GitClient,
    44  		Prompter:   f.Prompter,
    45  	}
    46  
    47  	var tokenStdin bool
    48  
    49  	cmd := &cobra.Command{
    50  		Use:   "login",
    51  		Args:  cobra.ExactArgs(0),
    52  		Short: "Authenticate with a GitHub host",
    53  		Long: heredoc.Docf(`
    54  			Authenticate with a GitHub host.
    55  
    56  			The default authentication mode is a web-based browser flow. After completion, an
    57  			authentication token will be stored internally.
    58  
    59  			Alternatively, use %[1]s--with-token%[1]s to pass in a token on standard input.
    60  			The minimum required scopes for the token are: "repo", "read:org".
    61  
    62  			Alternatively, gh will use the authentication token found in environment variables.
    63  			This method is most suitable for "headless" use of gh such as in automation. See
    64  			%[1]sgh help environment%[1]s for more info.
    65  
    66  			To use gh in GitHub Actions, add %[1]sGH_TOKEN: ${{ github.token }}%[1]s to "env".
    67  		`, "`"),
    68  		Example: heredoc.Doc(`
    69  			# start interactive setup
    70  			$ gh auth login
    71  
    72  			# authenticate against github.com by reading the token from a file
    73  			$ gh auth login --with-token < mytoken.txt
    74  
    75  			# authenticate with a specific GitHub instance
    76  			$ gh auth login --hostname enterprise.internal
    77  		`),
    78  		RunE: func(cmd *cobra.Command, args []string) error {
    79  			if tokenStdin && opts.Web {
    80  				return cmdutil.FlagErrorf("specify only one of `--web` or `--with-token`")
    81  			}
    82  			if tokenStdin && len(opts.Scopes) > 0 {
    83  				return cmdutil.FlagErrorf("specify only one of `--scopes` or `--with-token`")
    84  			}
    85  
    86  			if tokenStdin {
    87  				defer opts.IO.In.Close()
    88  				token, err := io.ReadAll(opts.IO.In)
    89  				if err != nil {
    90  					return fmt.Errorf("failed to read token from standard input: %w", err)
    91  				}
    92  				opts.Token = strings.TrimSpace(string(token))
    93  			}
    94  
    95  			if opts.IO.CanPrompt() && opts.Token == "" {
    96  				opts.Interactive = true
    97  			}
    98  
    99  			if cmd.Flags().Changed("hostname") {
   100  				if err := ghinstance.HostnameValidator(opts.Hostname); err != nil {
   101  					return cmdutil.FlagErrorf("error parsing hostname: %w", err)
   102  				}
   103  			}
   104  
   105  			if opts.Hostname == "" && (!opts.Interactive || opts.Web) {
   106  				opts.Hostname, _ = ghAuth.DefaultHost()
   107  			}
   108  
   109  			opts.MainExecutable = f.Executable()
   110  			if runF != nil {
   111  				return runF(opts)
   112  			}
   113  
   114  			return loginRun(opts)
   115  		},
   116  	}
   117  
   118  	cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to authenticate with")
   119  	cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes to request")
   120  	cmd.Flags().BoolVar(&tokenStdin, "with-token", false, "Read token from standard input")
   121  	cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a browser to authenticate")
   122  	cmdutil.StringEnumFlag(cmd, &opts.GitProtocol, "git-protocol", "p", "", []string{"ssh", "https"}, "The protocol to use for git operations")
   123  
   124  	return cmd
   125  }
   126  
   127  func loginRun(opts *LoginOptions) error {
   128  	cfg, err := opts.Config()
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	hostname := opts.Hostname
   134  	if opts.Interactive && hostname == "" {
   135  		var err error
   136  		hostname, err = promptForHostname(opts)
   137  		if err != nil {
   138  			return err
   139  		}
   140  	}
   141  
   142  	if src, writeable := shared.AuthTokenWriteable(cfg, hostname); !writeable {
   143  		fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", src)
   144  		fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI store credentials instead, first clear the value from the environment.\n")
   145  		return cmdutil.SilentError
   146  	}
   147  
   148  	httpClient, err := opts.HttpClient()
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	if opts.Token != "" {
   154  		cfg.Set(hostname, "oauth_token", opts.Token)
   155  
   156  		if err := shared.HasMinimumScopes(httpClient, hostname, opts.Token); err != nil {
   157  			return fmt.Errorf("error validating token: %w", err)
   158  		}
   159  		if opts.GitProtocol != "" {
   160  			cfg.Set(hostname, "git_protocol", opts.GitProtocol)
   161  		}
   162  		return cfg.Write()
   163  	}
   164  
   165  	existingToken, _ := cfg.AuthToken(hostname)
   166  	if existingToken != "" && opts.Interactive {
   167  		if err := shared.HasMinimumScopes(httpClient, hostname, existingToken); err == nil {
   168  			keepGoing, err := opts.Prompter.Confirm(fmt.Sprintf("You're already logged into %s. Do you want to re-authenticate?", hostname), false)
   169  			if err != nil {
   170  				return err
   171  			}
   172  			if !keepGoing {
   173  				return nil
   174  			}
   175  		}
   176  	}
   177  
   178  	return shared.Login(&shared.LoginOptions{
   179  		IO:          opts.IO,
   180  		Config:      cfg,
   181  		HTTPClient:  httpClient,
   182  		Hostname:    hostname,
   183  		Interactive: opts.Interactive,
   184  		Web:         opts.Web,
   185  		Scopes:      opts.Scopes,
   186  		Executable:  opts.MainExecutable,
   187  		GitProtocol: opts.GitProtocol,
   188  		Prompter:    opts.Prompter,
   189  		GitClient:   opts.GitClient,
   190  	})
   191  }
   192  
   193  func promptForHostname(opts *LoginOptions) (string, error) {
   194  	options := []string{"GitHub.com", "GitHub Enterprise Server"}
   195  	hostType, err := opts.Prompter.Select(
   196  		"What account do you want to log into?",
   197  		options[0],
   198  		options)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  
   203  	isEnterprise := hostType == 1
   204  
   205  	hostname := ghinstance.Default()
   206  	if isEnterprise {
   207  		hostname, err = opts.Prompter.InputHostname()
   208  	}
   209  
   210  	return hostname, err
   211  }