Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion internal/ghcmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/cli/cli/v2/internal/gh/ghtelemetry"
"github.com/cli/cli/v2/internal/telemetry"
"github.com/cli/cli/v2/internal/update"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/cli/cli/v2/pkg/cmd/root"
"github.com/cli/cli/v2/pkg/cmdutil"
Expand Down Expand Up @@ -227,7 +228,11 @@ func Main() exitCode {

var httpErr api.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
fmt.Fprintln(stderr, "Try authenticating with: gh auth login")
authCommand := "gh auth login"
if cfg, cfgErr := cmdFactory.Config(); cfgErr == nil {
authCommand = authRecoveryCommand(cfg, httpErr)
}
fmt.Fprintf(stderr, "Try authenticating with: %s\n", authCommand)
} else if u := factory.SSOURL(); u != "" {
// handles organization SAML enforcement error
fmt.Fprintf(stderr, "Authorize in your web browser: %s\n", u)
Expand Down Expand Up @@ -291,6 +296,20 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
}
}

func authRecoveryCommand(cfg gh.Config, httpErr api.HTTPError) string {
if httpErr.RequestURL == nil {
return "gh auth login"
}

hostname := ghauth.NormalizeHostname(httpErr.RequestURL.Hostname())
token, source := cfg.Authentication().ActiveToken(hostname)
if shared.AuthTokenRefreshable(token, source) {
return fmt.Sprintf("gh auth refresh -h %s", hostname)
}

return fmt.Sprintf("gh auth login -h %s", hostname)
}

func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) {
if updaterEnabled == "" || !update.ShouldCheckForUpdate() {
return nil, nil
Expand Down
75 changes: 75 additions & 0 deletions internal/ghcmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import (
"errors"
"fmt"
"net"
"net/url"
"testing"

"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/pkg/cmdutil"
ghAPI "github.com/cli/go-gh/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -511,3 +515,74 @@ func disableColorLabelsConfig() gh.Config {
func enableColorLabelsConfig() gh.Config {
return config.NewFromString("color_labels: enabled")
}

func Test_authRecoveryCommand(t *testing.T) {
tests := []struct {
name string
token string
source string
requestURL string
want string
}{
{
name: "stored oauth token",
token: "gho_abc123",
source: "oauth_token",
requestURL: "https://api.github.com/graphql",
want: "gh auth refresh -h github.com",
},
{
name: "stored pat",
token: "github_pat_abc123",
source: "oauth_token",
requestURL: "https://api.github.com/graphql",
want: "gh auth login -h github.com",
},
{
name: "env token",
token: "gho_abc123",
source: "GH_TOKEN",
requestURL: "https://api.github.com/graphql",
want: "gh auth login -h github.com",
},
{
name: "missing request url",
token: "gho_abc123",
source: "oauth_token",
want: "gh auth login",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authCfg := config.NewBlankConfig().Authentication()
authCfg.SetActiveToken(tt.token, tt.source)
cfg := &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return authCfg
},
}

var requestURL *url.URL
if tt.requestURL != "" {
var err error
requestURL, err = url.Parse(tt.requestURL)
if err != nil {
t.Fatalf("failed to parse request URL: %v", err)
}
}

httpErr := api.HTTPError{
HTTPError: &ghAPI.HTTPError{
RequestURL: requestURL,
StatusCode: 401,
},
}

got := authRecoveryCommand(cfg, httpErr)
if got != tt.want {
t.Errorf("authRecoveryCommand() = %q, want %q", got, tt.want)
}
})
}
}
6 changes: 6 additions & 0 deletions pkg/cmd/auth/shared/writeable.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import (
"github.com/cli/cli/v2/internal/gh"
)

// AuthTokenRefreshable reports whether the token is stored by gh and can be
// renewed with `gh auth refresh`.
func AuthTokenRefreshable(token, src string) bool {
return token != "" && !strings.HasSuffix(src, "_TOKEN") && strings.HasPrefix(token, "gho_")
}

func AuthTokenWriteable(authCfg gh.AuthConfig, hostname string) (string, bool) {
token, src := authCfg.ActiveToken(hostname)
return src, (token == "" || !strings.HasSuffix(src, "_TOKEN"))
Expand Down
3 changes: 3 additions & 0 deletions pkg/cmd/auth/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ func (e authEntry) String(cs *iostreams.ColorScheme) string {
sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.TokenSource))
if authTokenWriteable(e.TokenSource) {
loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host)
if shared.AuthTokenRefreshable(e.Token, e.TokenSource) {
loginInstructions = fmt.Sprintf("gh auth refresh -h %s", e.Host)
}
logoutInstructions := fmt.Sprintf("gh auth logout -h %s -u %s", e.Host, e.Login)
sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions)))
sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions)))
Expand Down
8 changes: 4 additions & 4 deletions pkg/cmd/auth/status/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
- Active account: true
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe
`),
},
Expand Down Expand Up @@ -229,7 +229,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
- Active account: true
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe
`),
},
Expand Down Expand Up @@ -447,7 +447,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
- Active account: false
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe
`),
},
Expand Down Expand Up @@ -535,7 +535,7 @@ func Test_statusRun(t *testing.T) {
X Failed to log in to ghe.io account monalisa-ghe-2 (GH_CONFIG_DIR/hosts.yml)
- Active account: true
- The token in GH_CONFIG_DIR/hosts.yml is invalid.
- To re-authenticate, run: gh auth login -h ghe.io
- To re-authenticate, run: gh auth refresh -h ghe.io
- To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe-2
`),
},
Expand Down
Loading