github.com/secman-team/gh-api@v1.8.2/pkg/cmd/auth/login/login.go (about) 1 package login 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "strings" 9 10 "github.com/AlecAivazis/survey/v2" 11 "github.com/MakeNowJust/heredoc" 12 "github.com/secman-team/gh-api/core/config" 13 "github.com/secman-team/gh-api/core/ghinstance" 14 "github.com/secman-team/gh-api/pkg/cmd/auth/shared" 15 "github.com/secman-team/gh-api/pkg/cmdutil" 16 "github.com/secman-team/gh-api/pkg/iostreams" 17 "github.com/secman-team/gh-api/pkg/prompt" 18 "github.com/spf13/cobra" 19 ) 20 21 type LoginOptions struct { 22 IO *iostreams.IOStreams 23 Config func() (config.Config, error) 24 HttpClient func() (*http.Client, error) 25 26 MainExecutable string 27 28 Interactive bool 29 30 Hostname string 31 Scopes []string 32 Token string 33 Web bool 34 } 35 36 func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command { 37 opts := &LoginOptions{ 38 IO: f.IOStreams, 39 Config: f.Config, 40 HttpClient: f.HttpClient, 41 42 MainExecutable: f.Executable, 43 } 44 45 var tokenStdin bool 46 47 cmd := &cobra.Command{ 48 Use: "login", 49 Args: cobra.ExactArgs(0), 50 Short: "Authenticate with a GitHub host", 51 Long: heredoc.Docf(` 52 Authenticate with a GitHub host. 53 54 The default authentication mode is a web-based browser flow. 55 56 Alternatively, pass in a token on standard input by using %[1]s--with-token%[1]s. 57 The minimum required scopes for the token are: "repo", "read:org". 58 59 The --scopes flag accepts a comma separated list of scopes you want your gh credentials to have. If 60 absent, this command ensures that gh has access to a minimum set of scopes. 61 `, "`"), 62 Example: heredoc.Doc(` 63 # start interactive setup 64 $ gh auth login 65 66 # authenticate against github.com by reading the token from a file 67 $ gh auth login --with-token < mytoken.txt 68 69 # authenticate with a specific GitHub Enterprise Server instance 70 $ gh auth login --hostname enterprise.internal 71 `), 72 RunE: func(cmd *cobra.Command, args []string) error { 73 if !opts.IO.CanPrompt() && !(tokenStdin || opts.Web) { 74 return &cmdutil.FlagError{Err: errors.New("--web or --with-token required when not running interactively")} 75 } 76 77 if tokenStdin && opts.Web { 78 return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --with-token")} 79 } 80 81 if tokenStdin { 82 defer opts.IO.In.Close() 83 token, err := ioutil.ReadAll(opts.IO.In) 84 if err != nil { 85 return fmt.Errorf("failed to read token from STDIN: %w", err) 86 } 87 opts.Token = strings.TrimSpace(string(token)) 88 } 89 90 if opts.IO.CanPrompt() && opts.Token == "" && !opts.Web { 91 opts.Interactive = true 92 } 93 94 if cmd.Flags().Changed("hostname") { 95 if err := ghinstance.HostnameValidator(opts.Hostname); err != nil { 96 return &cmdutil.FlagError{Err: fmt.Errorf("error parsing --hostname: %w", err)} 97 } 98 } 99 100 if !opts.Interactive { 101 if opts.Hostname == "" { 102 opts.Hostname = ghinstance.Default() 103 } 104 } 105 106 if runF != nil { 107 return runF(opts) 108 } 109 110 return loginRun(opts) 111 }, 112 } 113 114 cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to authenticate with") 115 cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have") 116 cmd.Flags().BoolVar(&tokenStdin, "with-token", false, "Read token from standard input") 117 cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a browser to authenticate") 118 119 return cmd 120 } 121 122 func loginRun(opts *LoginOptions) error { 123 cfg, err := opts.Config() 124 if err != nil { 125 return err 126 } 127 128 hostname := opts.Hostname 129 if hostname == "" { 130 if opts.Interactive { 131 var err error 132 hostname, err = promptForHostname() 133 if err != nil { 134 return err 135 } 136 } else { 137 return errors.New("must specify --hostname") 138 } 139 } 140 141 if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil { 142 var roErr *config.ReadOnlyEnvError 143 if errors.As(err, &roErr) { 144 fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable) 145 fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI store credentials instead, first clear the value from the environment.\n") 146 return cmdutil.SilentError 147 } 148 return err 149 } 150 151 httpClient, err := opts.HttpClient() 152 if err != nil { 153 return err 154 } 155 156 if opts.Token != "" { 157 err := cfg.Set(hostname, "oauth_token", opts.Token) 158 if err != nil { 159 return err 160 } 161 162 if err := shared.HasMinimumScopes(httpClient, hostname, opts.Token); err != nil { 163 return fmt.Errorf("error validating token: %w", err) 164 } 165 166 return cfg.Write() 167 } 168 169 existingToken, _ := cfg.Get(hostname, "oauth_token") 170 if existingToken != "" && opts.Interactive { 171 if err := shared.HasMinimumScopes(httpClient, hostname, existingToken); err == nil { 172 var keepGoing bool 173 err = prompt.SurveyAskOne(&survey.Confirm{ 174 Message: fmt.Sprintf( 175 "You're already logged into %s. Do you want to re-authenticate?", 176 hostname), 177 Default: false, 178 }, &keepGoing) 179 if err != nil { 180 return fmt.Errorf("could not prompt: %w", err) 181 } 182 if !keepGoing { 183 return nil 184 } 185 } 186 } 187 188 return shared.Login(&shared.LoginOptions{ 189 IO: opts.IO, 190 Config: cfg, 191 HTTPClient: httpClient, 192 Hostname: hostname, 193 Interactive: opts.Interactive, 194 Web: opts.Web, 195 Scopes: opts.Scopes, 196 Executable: opts.MainExecutable, 197 }) 198 } 199 200 func promptForHostname() (string, error) { 201 var hostType int 202 err := prompt.SurveyAskOne(&survey.Select{ 203 Message: "What account do you want to log into?", 204 Options: []string{ 205 "GitHub.com", 206 "GitHub Enterprise Server", 207 }, 208 }, &hostType) 209 210 if err != nil { 211 return "", fmt.Errorf("could not prompt: %w", err) 212 } 213 214 isEnterprise := hostType == 1 215 216 hostname := ghinstance.Default() 217 if isEnterprise { 218 err := prompt.SurveyAskOne(&survey.Input{ 219 Message: "GHE hostname:", 220 }, &hostname, survey.WithValidator(ghinstance.HostnameValidator)) 221 if err != nil { 222 return "", fmt.Errorf("could not prompt: %w", err) 223 } 224 } 225 226 return hostname, nil 227 }