github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/internal/authflow/flow.go (about)

     1  package authflow
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/cli/cli/api"
    12  	"github.com/cli/cli/internal/ghinstance"
    13  	"github.com/cli/cli/pkg/cmdutil"
    14  	"github.com/cli/cli/pkg/iostreams"
    15  	"github.com/cli/oauth"
    16  )
    17  
    18  var (
    19  	// The "GitHub CLI" OAuth app
    20  	oauthClientID = "178c6fc778ccc68e1d6a"
    21  	// This value is safe to be embedded in version control
    22  	oauthClientSecret = "34ddeff2b558a23d38fba8a6de74f086ede1cc0b"
    23  )
    24  
    25  type iconfig interface {
    26  	Set(string, string, string) error
    27  	Write() error
    28  }
    29  
    30  func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice string, additionalScopes []string) (string, error) {
    31  	// TODO this probably shouldn't live in this package. It should probably be in a new package that
    32  	// depends on both iostreams and config.
    33  	stderr := IO.ErrOut
    34  	cs := IO.ColorScheme()
    35  
    36  	token, userLogin, err := authFlow(hostname, IO, notice, additionalScopes)
    37  	if err != nil {
    38  		return "", err
    39  	}
    40  
    41  	err = cfg.Set(hostname, "user", userLogin)
    42  	if err != nil {
    43  		return "", err
    44  	}
    45  	err = cfg.Set(hostname, "oauth_token", token)
    46  	if err != nil {
    47  		return "", err
    48  	}
    49  
    50  	err = cfg.Write()
    51  	if err != nil {
    52  		return "", err
    53  	}
    54  
    55  	fmt.Fprintf(stderr, "%s Authentication complete. %s to continue...\n",
    56  		cs.SuccessIcon(), cs.Bold("Press Enter"))
    57  	_ = waitForEnter(IO.In)
    58  
    59  	return token, nil
    60  }
    61  
    62  func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string) (string, string, error) {
    63  	w := IO.ErrOut
    64  	cs := IO.ColorScheme()
    65  
    66  	httpClient := http.DefaultClient
    67  	if envDebug := os.Getenv("DEBUG"); envDebug != "" {
    68  		logTraffic := strings.Contains(envDebug, "api") || strings.Contains(envDebug, "oauth")
    69  		httpClient.Transport = api.VerboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport)
    70  	}
    71  
    72  	minimumScopes := []string{"repo", "read:org", "gist"}
    73  	scopes := append(minimumScopes, additionalScopes...)
    74  
    75  	callbackURI := "http://127.0.0.1/callback"
    76  	if ghinstance.IsEnterprise(oauthHost) {
    77  		// the OAuth app on Enterprise hosts is still registered with a legacy callback URL
    78  		// see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650
    79  		callbackURI = "http://localhost/"
    80  	}
    81  
    82  	flow := &oauth.Flow{
    83  		Hostname:     oauthHost,
    84  		ClientID:     oauthClientID,
    85  		ClientSecret: oauthClientSecret,
    86  		CallbackURI:  callbackURI,
    87  		Scopes:       scopes,
    88  		DisplayCode: func(code, verificationURL string) error {
    89  			fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code))
    90  			return nil
    91  		},
    92  		BrowseURL: func(url string) error {
    93  			fmt.Fprintf(w, "- %s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost)
    94  			_ = waitForEnter(IO.In)
    95  
    96  			// FIXME: read the browser from cmd Factory rather than recreating it
    97  			browser := cmdutil.NewBrowser(os.Getenv("BROWSER"), IO.Out, IO.ErrOut)
    98  			if err := browser.Browse(url); err != nil {
    99  				fmt.Fprintf(w, "%s Failed opening a web browser at %s\n", cs.Red("!"), url)
   100  				fmt.Fprintf(w, "  %s\n", err)
   101  				fmt.Fprint(w, "  Please try entering the URL in your browser manually\n")
   102  			}
   103  			return nil
   104  		},
   105  		WriteSuccessHTML: func(w io.Writer) {
   106  			fmt.Fprintln(w, oauthSuccessPage)
   107  		},
   108  		HTTPClient: httpClient,
   109  		Stdin:      IO.In,
   110  		Stdout:     w,
   111  	}
   112  
   113  	fmt.Fprintln(w, notice)
   114  
   115  	token, err := flow.DetectFlow()
   116  	if err != nil {
   117  		return "", "", err
   118  	}
   119  
   120  	userLogin, err := getViewer(oauthHost, token.Token)
   121  	if err != nil {
   122  		return "", "", err
   123  	}
   124  
   125  	return token.Token, userLogin, nil
   126  }
   127  
   128  func getViewer(hostname, token string) (string, error) {
   129  	http := api.NewClient(api.AddHeader("Authorization", fmt.Sprintf("token %s", token)))
   130  	return api.CurrentLoginName(http, hostname)
   131  }
   132  
   133  func waitForEnter(r io.Reader) error {
   134  	scanner := bufio.NewScanner(r)
   135  	scanner.Scan()
   136  	return scanner.Err()
   137  }