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 }