From f1c0d04bc0f3bbcb7f415392fab61ec7affab35d Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 7 Aug 2020 12:37:58 -0500 Subject: [PATCH 1/5] gh auth refresh --- pkg/cmd/auth/logout/logout.go | 1 + pkg/cmd/auth/refresh/refresh.go | 127 ++++++++++++++ pkg/cmd/auth/refresh/refresh_test.go | 249 +++++++++++++++++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 pkg/cmd/auth/refresh/refresh.go create mode 100644 pkg/cmd/auth/refresh/refresh_test.go diff --git a/pkg/cmd/auth/logout/logout.go b/pkg/cmd/auth/logout/logout.go index 744cd6a3f..5192f2bf7 100644 --- a/pkg/cmd/auth/logout/logout.go +++ b/pkg/cmd/auth/logout/logout.go @@ -62,6 +62,7 @@ func NewCmdLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobra.Co } func logoutRun(opts *LogoutOptions) error { + // TODO check for GITHUB_TOKEN and error if found isTTY := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() hostname := opts.Hostname diff --git a/pkg/cmd/auth/refresh/refresh.go b/pkg/cmd/auth/refresh/refresh.go new file mode 100644 index 000000000..61880cbba --- /dev/null +++ b/pkg/cmd/auth/refresh/refresh.go @@ -0,0 +1,127 @@ +package refresh + +import ( + "fmt" + "os" + + "github.com/AlecAivazis/survey/v2" + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/pkg/prompt" + "github.com/spf13/cobra" +) + +type RefreshOptions struct { + IO *iostreams.IOStreams + Config func() (config.Config, error) + + Hostname string + Scopes []string +} + +func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.Command { + opts := &RefreshOptions{ + IO: f.IOStreams, + Config: f.Config, + } + + cmd := &cobra.Command{ + Use: "refresh", + Args: cobra.ExactArgs(0), + Short: "Request new scopes for a token", + Long: heredoc.Doc(`Expand the permission scopes for a given host's token. + + This command allows you to add additional scopes to an existing authentication token via a web + browser. This enables gh to access more of the GitHub API, which may be required as gh adds + features or as you use the gh api command. + + Unfortunately at this time there is no way to add scopes without a web browser's involvement + due to how GitHub authentication works. + + The --hostname flag allows you to operate on a GitHub host other than github.com. + + The --scopes flag accepts a comma separated list of scopes you want to add to a token. If + absent, this command ensures that a host's token has the default set of scopes required by gh. + + Note that if GITHUB_TOKEN is in the current environment, this command will not work. + `), + Example: heredoc.Doc(` + $ gh auth refresh --scopes write:org,read:public_key + # => open a browser to add write:org and read:public_key scopes for use with gh api + + $ gh auth refresh + # => ensure that the required minimum scopes are enabled for a token and open a browser to add if not + `), + RunE: func(cmd *cobra.Command, args []string) error { + if runF != nil { + return runF(opts) + } + + return refreshRun(opts) + }, + } + + cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The GitHub host to use for authentication") + cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", []string{}, "Additional scopes to add to a token") + + return cmd +} + +func refreshRun(opts *RefreshOptions) error { + if os.Getenv("GITHUB_TOKEN") != "" { + return fmt.Errorf("GITHUB_TOKEN is present in your environment and is incompatible with this command. If you'd like to modify a personal access token, see https://github.com/settings/tokens") + } + + isTTY := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() + + if !isTTY { + return fmt.Errorf("not attached to a terminal; in headless environments, GITHUB_TOKEN is recommended") + } + + cfg, err := opts.Config() + if err != nil { + return err + } + + candidates, err := cfg.Hosts() + if err != nil { + return fmt.Errorf("not logged in to any hosts. Use 'gh auth login' to authenticate with a host") + } + + hostname := opts.Hostname + if hostname == "" { + if len(candidates) == 1 { + hostname = candidates[0] + } else { + err := prompt.SurveyAskOne(&survey.Select{ + Message: "What account do you want to refresh auth for?", + Options: candidates, + }, &hostname) + + if err != nil { + return fmt.Errorf("could not prompt: %w", err) + } + } + } else { + var found bool + for _, c := range candidates { + if c == hostname { + found = true + break + } + } + + if !found { + return fmt.Errorf("not logged in to %s. use 'gh auth login' to authenticate with this host", hostname) + } + } + + return doAuthFlow(cfg, hostname, opts.Scopes) +} + +var doAuthFlow = func(cfg config.Config, hostname string, scopes []string) error { + _, err := config.AuthFlowWithConfig(cfg, hostname, "", scopes) + return err +} diff --git a/pkg/cmd/auth/refresh/refresh_test.go b/pkg/cmd/auth/refresh/refresh_test.go new file mode 100644 index 000000000..1c29b2024 --- /dev/null +++ b/pkg/cmd/auth/refresh/refresh_test.go @@ -0,0 +1,249 @@ +package refresh + +import ( + "bytes" + "os" + "regexp" + "testing" + + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/pkg/prompt" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func Test_NewCmdRefresh(t *testing.T) { + tests := []struct { + name string + cli string + wants RefreshOptions + }{ + { + name: "no arguments", + wants: RefreshOptions{ + Hostname: "", + Scopes: []string{}, + }, + }, + { + name: "hostname", + cli: "-h aline.cedrac", + wants: RefreshOptions{ + Hostname: "aline.cedrac", + Scopes: []string{}, + }, + }, + { + name: "one scope", + cli: "--scopes repo:invite", + wants: RefreshOptions{ + Scopes: []string{"repo:invite"}, + }, + }, + { + name: "scopes", + cli: "--scopes repo:invite,read:public_key", + wants: RefreshOptions{ + Scopes: []string{"repo:invite", "read:public_key"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: io, + } + + argv, err := shlex.Split(tt.cli) + assert.NoError(t, err) + + var gotOpts *RefreshOptions + cmd := NewCmdRefresh(f, func(opts *RefreshOptions) error { + gotOpts = opts + return nil + }) + // TODO cobra hack-around + cmd.Flags().BoolP("help", "x", false, "") + + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + assert.NoError(t, err) + assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) + assert.Equal(t, tt.wants.Scopes, gotOpts.Scopes) + }) + + } +} + +type authArgs struct { + hostname string + scopes []string +} + +func Test_refreshRun(t *testing.T) { + tests := []struct { + name string + opts *RefreshOptions + askStubs func(*prompt.AskStubber) + cfgHosts []string + wantErr *regexp.Regexp + ghtoken string + nontty bool + wantAuthArgs authArgs + }{ + { + name: "GITHUB_TOKEN set", + opts: &RefreshOptions{}, + ghtoken: "abc123", + wantErr: regexp.MustCompile(`GITHUB_TOKEN is present in your environment`), + }, + { + name: "non tty", + opts: &RefreshOptions{}, + nontty: true, + wantErr: regexp.MustCompile(`not attached to a terminal;`), + }, + { + name: "no hosts configured", + opts: &RefreshOptions{}, + wantErr: regexp.MustCompile(`not logged in to any hosts`), + }, + { + name: "hostname given but dne", + cfgHosts: []string{ + "github.com", + "aline.cedrac", + }, + opts: &RefreshOptions{ + Hostname: "obed.morton", + }, + wantErr: regexp.MustCompile(`not logged in to obed.morton`), + }, + { + name: "hostname provided and is configured", + cfgHosts: []string{ + "obed.morton", + "github.com", + }, + opts: &RefreshOptions{ + Hostname: "obed.morton", + Scopes: []string{}, + }, + wantAuthArgs: authArgs{ + hostname: "obed.morton", + scopes: []string{}, + }, + }, + { + name: "no hostname, one host configured", + cfgHosts: []string{ + "github.com", + }, + opts: &RefreshOptions{ + Hostname: "", + Scopes: []string{}, + }, + wantAuthArgs: authArgs{ + hostname: "github.com", + scopes: []string{}, + }, + }, + { + name: "no hostname, multiple hosts configured", + cfgHosts: []string{ + "github.com", + "aline.cedrac", + }, + opts: &RefreshOptions{ + Hostname: "", + Scopes: []string{}, + }, + askStubs: func(as *prompt.AskStubber) { + as.StubOne("github.com") + }, + wantAuthArgs: authArgs{ + hostname: "github.com", + scopes: []string{}, + }, + }, + { + name: "scopes provided", + cfgHosts: []string{ + "github.com", + }, + opts: &RefreshOptions{ + Scopes: []string{"repo:invite", "public_key:read"}, + }, + wantAuthArgs: authArgs{ + hostname: "github.com", + scopes: []string{"repo:invite", "public_key:read"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aa := authArgs{} + doAuthFlow = func(_ config.Config, hostname string, scopes []string) error { + aa.hostname = hostname + aa.scopes = scopes + return nil + } + + ghtoken := os.Getenv("GITHUB_TOKEN") + defer func() { + os.Setenv("GITHUB_TOKEN", ghtoken) + }() + os.Setenv("GITHUB_TOKEN", tt.ghtoken) + io, _, _, _ := iostreams.Test() + + io.SetStdinTTY(!tt.nontty) + io.SetStdoutTTY(!tt.nontty) + + tt.opts.IO = io + cfg := config.NewBlankConfig() + tt.opts.Config = func() (config.Config, error) { + return cfg, nil + } + for _, hostname := range tt.cfgHosts { + _ = cfg.Set(hostname, "oauth_token", "abc123") + } + reg := &httpmock.Registry{} + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"cybilb"}}}`)) + + mainBuf := bytes.Buffer{} + hostsBuf := bytes.Buffer{} + defer config.StubWriteConfig(&mainBuf, &hostsBuf)() + + as, teardown := prompt.InitAskStubber() + defer teardown() + if tt.askStubs != nil { + tt.askStubs(as) + } + + err := refreshRun(tt.opts) + assert.Equal(t, tt.wantErr == nil, err == nil) + if err != nil { + if tt.wantErr != nil { + assert.True(t, tt.wantErr.MatchString(err.Error())) + return + } else { + t.Fatalf("unexpected error: %s", err) + } + } + + assert.Equal(t, aa.hostname, tt.wantAuthArgs.hostname) + assert.Equal(t, aa.scopes, tt.wantAuthArgs.scopes) + }) + } +} From e6ae0a122b1f7a502b75bf4fa61ee4cea691d263 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 10 Aug 2020 12:48:22 -0500 Subject: [PATCH 2/5] accept additional scopes in auth flow --- internal/config/config_setup.go | 11 +++++++---- pkg/cmd/auth/login/login.go | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/config/config_setup.go b/internal/config/config_setup.go index d8fcef1ec..2aaf597c8 100644 --- a/internal/config/config_setup.go +++ b/internal/config/config_setup.go @@ -26,8 +26,8 @@ func IsGitHubApp(id string) bool { return id == "178c6fc778ccc68e1d6a" || id == "4d747ba5675d5d66553f" } -func AuthFlowWithConfig(cfg Config, hostname, notice string) (string, error) { - token, userLogin, err := authFlow(hostname, notice) +func AuthFlowWithConfig(cfg Config, hostname, notice string, additionalScopes []string) (string, error) { + token, userLogin, err := authFlow(hostname, notice, additionalScopes) if err != nil { return "", err } @@ -50,17 +50,20 @@ func AuthFlowWithConfig(cfg Config, hostname, notice string) (string, error) { return token, nil } -func authFlow(oauthHost, notice string) (string, string, error) { +func authFlow(oauthHost, notice string, additionalScopes []string) (string, string, error) { var verboseStream io.Writer if strings.Contains(os.Getenv("DEBUG"), "oauth") { verboseStream = os.Stderr } + minimumScopes := []string{"repo", "read:org", "gist"} + scopes := append(minimumScopes, additionalScopes...) + flow := &auth.OAuthFlow{ Hostname: oauthHost, ClientID: oauthClientID, ClientSecret: oauthClientSecret, - Scopes: []string{"repo", "read:org", "gist"}, + Scopes: scopes, WriteSuccessHTML: func(w io.Writer) { fmt.Fprintln(w, oauthSuccessPage) }, diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index cd43c536c..7d53839c6 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -210,7 +210,7 @@ func loginRun(opts *LoginOptions) error { } if authMode == 0 { - _, err := config.AuthFlowWithConfig(cfg, hostname, "") + _, err := config.AuthFlowWithConfig(cfg, hostname, "", []string{}) if err != nil { return fmt.Errorf("failed to authenticate via web browser: %w", err) } From f7ee39dfeb72be9fc5fa4a5adb49f7e17e4862e9 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 10 Aug 2020 13:01:20 -0500 Subject: [PATCH 3/5] bonus: logout error if GITHUB_TOKEN is set --- pkg/cmd/auth/logout/logout.go | 6 +++++- pkg/cmd/auth/logout/logout_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/auth/logout/logout.go b/pkg/cmd/auth/logout/logout.go index 5192f2bf7..6f047a079 100644 --- a/pkg/cmd/auth/logout/logout.go +++ b/pkg/cmd/auth/logout/logout.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "os" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" @@ -62,7 +63,10 @@ func NewCmdLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobra.Co } func logoutRun(opts *LogoutOptions) error { - // TODO check for GITHUB_TOKEN and error if found + if os.Getenv("GITHUB_TOKEN") != "" { + return errors.New("GITHUB_TOKEN is set in your environment. If you no longer want to use it with gh, please unset it.") + } + isTTY := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY() hostname := opts.Hostname diff --git a/pkg/cmd/auth/logout/logout_test.go b/pkg/cmd/auth/logout/logout_test.go index 83d13aad3..50e1bed2a 100644 --- a/pkg/cmd/auth/logout/logout_test.go +++ b/pkg/cmd/auth/logout/logout_test.go @@ -3,6 +3,7 @@ package logout import ( "bytes" "net/http" + "os" "regexp" "testing" @@ -183,6 +184,7 @@ func Test_logoutRun_nontty(t *testing.T) { cfgHosts []string wantHosts string wantErr *regexp.Regexp + ghtoken string }{ { name: "no arguments", @@ -211,10 +213,21 @@ func Test_logoutRun_nontty(t *testing.T) { }, wantErr: regexp.MustCompile(`not logged in to any hosts`), }, + { + name: "gh token is set", + opts: &LogoutOptions{}, + ghtoken: "abc123", + wantErr: regexp.MustCompile(`GITHUB_TOKEN is set in your environment`), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ghtoken := os.Getenv("GITHUB_TOKEN") + defer func() { + os.Setenv("GITHUB_TOKEN", ghtoken) + }() + os.Setenv("GITHUB_TOKEN", tt.ghtoken) io, _, _, stderr := iostreams.Test() io.SetStdinTTY(false) From 2f1e9ecc7a67182510de254e189483efd4158550 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 12 Aug 2020 12:02:58 -0500 Subject: [PATCH 4/5] review feedback --- pkg/cmd/auth/refresh/refresh.go | 35 ++++++++++------------------ pkg/cmd/auth/refresh/refresh_test.go | 13 ++++------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/pkg/cmd/auth/refresh/refresh.go b/pkg/cmd/auth/refresh/refresh.go index 61880cbba..8a69ea0e2 100644 --- a/pkg/cmd/auth/refresh/refresh.go +++ b/pkg/cmd/auth/refresh/refresh.go @@ -19,40 +19,34 @@ type RefreshOptions struct { Hostname string Scopes []string + AuthFlow func(config.Config, string, []string) error } func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.Command { opts := &RefreshOptions{ IO: f.IOStreams, Config: f.Config, + AuthFlow: func(cfg config.Config, hostname string, scopes []string) error { + _, err := config.AuthFlowWithConfig(cfg, hostname, "", scopes) + return err + }, } cmd := &cobra.Command{ Use: "refresh", Args: cobra.ExactArgs(0), - Short: "Request new scopes for a token", - Long: heredoc.Doc(`Expand the permission scopes for a given host's token. + Short: "Refresh stored authentication credentials", + Long: heredoc.Doc(`Expand or fix the permission scopes for stored credentials - This command allows you to add additional scopes to an existing authentication token via a web - browser. This enables gh to access more of the GitHub API, which may be required as gh adds - features or as you use the gh api command. - - Unfortunately at this time there is no way to add scopes without a web browser's involvement - due to how GitHub authentication works. - - The --hostname flag allows you to operate on a GitHub host other than github.com. - - The --scopes flag accepts a comma separated list of scopes you want to add to a token. If - absent, this command ensures that a host's token has the default set of scopes required by gh. - - Note that if GITHUB_TOKEN is in the current environment, this command will not work. + The --scopes flag accepts a comma separated list of scopes you want your gh credentials to have. If + absent, this command ensures that gh has access to a minimum set of scopes. `), Example: heredoc.Doc(` $ gh auth refresh --scopes write:org,read:public_key # => open a browser to add write:org and read:public_key scopes for use with gh api $ gh auth refresh - # => ensure that the required minimum scopes are enabled for a token and open a browser to add if not + # => open a browser to ensure your authentication credentials have the correct minimum scopes `), RunE: func(cmd *cobra.Command, args []string) error { if runF != nil { @@ -64,7 +58,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra. } cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The GitHub host to use for authentication") - cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", []string{}, "Additional scopes to add to a token") + cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have") return cmd } @@ -118,10 +112,5 @@ func refreshRun(opts *RefreshOptions) error { } } - return doAuthFlow(cfg, hostname, opts.Scopes) -} - -var doAuthFlow = func(cfg config.Config, hostname string, scopes []string) error { - _, err := config.AuthFlowWithConfig(cfg, hostname, "", scopes) - return err + return opts.AuthFlow(cfg, hostname, opts.Scopes) } diff --git a/pkg/cmd/auth/refresh/refresh_test.go b/pkg/cmd/auth/refresh/refresh_test.go index 1c29b2024..3a22d507f 100644 --- a/pkg/cmd/auth/refresh/refresh_test.go +++ b/pkg/cmd/auth/refresh/refresh_test.go @@ -25,7 +25,6 @@ func Test_NewCmdRefresh(t *testing.T) { name: "no arguments", wants: RefreshOptions{ Hostname: "", - Scopes: []string{}, }, }, { @@ -33,7 +32,6 @@ func Test_NewCmdRefresh(t *testing.T) { cli: "-h aline.cedrac", wants: RefreshOptions{ Hostname: "aline.cedrac", - Scopes: []string{}, }, }, { @@ -136,11 +134,10 @@ func Test_refreshRun(t *testing.T) { }, opts: &RefreshOptions{ Hostname: "obed.morton", - Scopes: []string{}, }, wantAuthArgs: authArgs{ hostname: "obed.morton", - scopes: []string{}, + scopes: nil, }, }, { @@ -150,11 +147,10 @@ func Test_refreshRun(t *testing.T) { }, opts: &RefreshOptions{ Hostname: "", - Scopes: []string{}, }, wantAuthArgs: authArgs{ hostname: "github.com", - scopes: []string{}, + scopes: nil, }, }, { @@ -165,14 +161,13 @@ func Test_refreshRun(t *testing.T) { }, opts: &RefreshOptions{ Hostname: "", - Scopes: []string{}, }, askStubs: func(as *prompt.AskStubber) { as.StubOne("github.com") }, wantAuthArgs: authArgs{ hostname: "github.com", - scopes: []string{}, + scopes: nil, }, }, { @@ -192,7 +187,7 @@ func Test_refreshRun(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { aa := authArgs{} - doAuthFlow = func(_ config.Config, hostname string, scopes []string) error { + tt.opts.AuthFlow = func(_ config.Config, hostname string, scopes []string) error { aa.hostname = hostname aa.scopes = scopes return nil From 1efd3ebb514e23bd87ecbf20bbe9074edc9dff6f Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 12 Aug 2020 12:07:36 -0500 Subject: [PATCH 5/5] rebase strife --- pkg/cmd/auth/auth.go | 2 ++ pkg/cmd/factory/http.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index 7f2520631..67c6abb31 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -3,6 +3,7 @@ package auth import ( authLoginCmd "github.com/cli/cli/pkg/cmd/auth/login" authLogoutCmd "github.com/cli/cli/pkg/cmd/auth/logout" + authRefreshCmd "github.com/cli/cli/pkg/cmd/auth/refresh" authStatusCmd "github.com/cli/cli/pkg/cmd/auth/status" "github.com/cli/cli/pkg/cmdutil" "github.com/spf13/cobra" @@ -18,6 +19,7 @@ func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(authLoginCmd.NewCmdLogin(f, nil)) cmd.AddCommand(authLogoutCmd.NewCmdLogout(f, nil)) cmd.AddCommand(authStatusCmd.NewCmdStatus(f, nil)) + cmd.AddCommand(authRefreshCmd.NewCmdRefresh(f, nil)) return cmd } diff --git a/pkg/cmd/factory/http.go b/pkg/cmd/factory/http.go index 8a6dab198..b113100f7 100644 --- a/pkg/cmd/factory/http.go +++ b/pkg/cmd/factory/http.go @@ -35,7 +35,7 @@ func httpClient(io *iostreams.IOStreams, cfg config.Config, appVersion string, s // TODO: check if stdout is TTY too if errors.As(err, ¬Found) && io.IsStdinTTY() { // interactive OAuth flow - token, err = config.AuthFlowWithConfig(cfg, hostname, "Notice: authentication required") + token, err = config.AuthFlowWithConfig(cfg, hostname, "Notice: authentication required", nil) } if err != nil { return "", err