diff --git a/cmd/gateclient/client.go b/cmd/gateclient/client.go index b7becaa1..dd3ecfd8 100644 --- a/cmd/gateclient/client.go +++ b/cmd/gateclient/client.go @@ -388,10 +388,14 @@ func authenticateOAuth2(output func(string), httpClient *http.Client, endpoint s return false, errors.New("incorrect OAuth2 auth configuration") } + redirectURL := url.URL{ + Scheme: "http", + Host: localWebServer, + } config := &oauth2.Config{ ClientID: OAuth2.ClientId, ClientSecret: OAuth2.ClientSecret, - RedirectURL: "http://localhost:8085", + RedirectURL: redirectURL.String(), Scopes: OAuth2.Scopes, Endpoint: oauth2.Endpoint{ AuthURL: OAuth2.AuthUrl, @@ -409,14 +413,6 @@ func authenticateOAuth2(output func(string), httpClient *http.Client, endpoint s return false, errors.Wrapf(err, "Could not refresh token from source: %v", tokenSource) } } else { - // Do roundtrip. - http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - code := r.FormValue("code") - fmt.Fprintln(w, code) - })) - go http.ListenAndServe(":8085", nil) - // Note: leaving server connection open for scope of request, will be reaped on exit. - verifier, verifierCode, err := generateCodeVerifier() if err != nil { return false, err @@ -427,10 +423,8 @@ func authenticateOAuth2(output func(string), httpClient *http.Client, endpoint s challengeMethod := oauth2.SetAuthURLParam("code_challenge_method", "S256") authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.ApprovalForce, challengeMethod, codeChallenge) - output(fmt.Sprintf("Navigate to %s and authenticate", authURL)) - code := prompt(output, "Paste authorization code:") - - newToken, err = config.Exchange(context.Background(), code, codeVerifier) + output("Trying to get token from web") + newToken, err = getTokenFromWeb(output, config, authURL, codeVerifier) if err != nil { return false, err } diff --git a/cmd/gateclient/oauth.go b/cmd/gateclient/oauth.go new file mode 100644 index 00000000..cdb2a9d4 --- /dev/null +++ b/cmd/gateclient/oauth.go @@ -0,0 +1,85 @@ +package gateclient + +import ( + "context" + "fmt" + "log" + "net" + "net/http" + "os/exec" + "runtime" + + "golang.org/x/oauth2" +) + +const localWebServer = "localhost:8085" + +func startWebServer() (codeCh chan string, err error) { + listener, err := net.Listen("tcp", localWebServer) + if err != nil { + return nil, err + } + codeCh = make(chan string) + + go http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // TODO: add handle of `error` + //error := r.FormValue("error") + code := r.FormValue("code") + codeCh <- code // send code to OAuth flow + listener.Close() + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintf(w, "Received code: %v\r\nYou can now safely close this browser window.", code) + })) + + return codeCh, nil +} + +// getTokenFromWeb uses Config to request a Token. +// It returns the retrieved Token. +func getTokenFromWeb(output func(string), config *oauth2.Config, authURL string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + codeCh, err := startWebServer() + if err != nil { + output("Unable to start a web server.") + return nil, err + } + + err = openURL(authURL) + if err != nil { + log.Fatalf("Unable to open authorization URL in web server: %v", err) + } else { + output("Your browser has been opened to an authorization URL.\n" + + " This program will resume once authorization has been provided.\n") + output(authURL) + } + + // Wait for the web server to get the code. + code := <-codeCh + return exchangeToken(config, code, opts...) +} + +// Exchange the authorization code for an access token +func exchangeToken(config *oauth2.Config, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + tok, err := config.Exchange(context.Background(), code, opts...) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve token %v", err) + } + return tok, nil +} + +// openURL opens a browser window to the specified location. +// This code originally appeared at: +// http://stackoverflow.com/questions/10377243/how-can-i-launch-a-process-that-is-not-a-file-in-go +func openURL(url string) error { + var err error + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("Cannot open URL %s on this platform", url) + } + return err +}