diff --git a/cmd/gateclient/client.go b/cmd/gateclient/client.go index b7becaa1..2acfc04e 100644 --- a/cmd/gateclient/client.go +++ b/cmd/gateclient/client.go @@ -30,12 +30,15 @@ import ( _ "net/http/pprof" "net/url" "os" + "os/exec" "path/filepath" + "runtime" "strings" "syscall" "github.com/pkg/errors" "golang.org/x/crypto/ssh/terminal" + "golang.org/x/net/publicsuffix" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "sigs.k8s.io/yaml" @@ -187,7 +190,7 @@ func NewGateClient(ui output.Ui, gateEndpoint, defaultHeaders, configLocation st // TODO: Verify version compatibility between Spin CLI and Gate. _, _, err = gateClient.VersionControllerApi.GetVersionUsingGET(gateClient.Context) if err != nil { - ui.Error("Could not reach Gate, please ensure it is running. Failing.") + ui.Error(fmt.Sprintf("Could not reach Gate, please ensure it is running. Failing. %s", err)) return nil, err } @@ -238,7 +241,7 @@ func userConfig(gateClient *GatewayClient, configLocation string) error { // InitializeHTTPClient will return an *http.Client configured with // optional TLS keys as specified in the auth.Config func InitializeHTTPClient(auth *auth.Config) (*http.Client, error) { - cookieJar, _ := cookiejar.New(nil) + cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) client := http.Client{ Jar: cookieJar, Transport: http.DefaultTransport.(*http.Transport).Clone(), @@ -409,31 +412,51 @@ func authenticateOAuth2(output func(string), httpClient *http.Client, endpoint s return false, errors.Wrapf(err, "Could not refresh token from source: %v", tokenSource) } } else { + done := make(chan bool) + verifier, verifierCode, err := generateCodeVerifier() + codeVerifier := oauth2.SetAuthURLParam("code_verifier", verifier) + if err != nil { + return false, err + } + // Do roundtrip. http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { code := r.FormValue("code") - fmt.Fprintln(w, code) + newToken, err = config.Exchange(context.Background(), code, codeVerifier) + if err != nil { + errors.Wrapf(err, "Could not get the authentication token: %s", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } else { + done <- true + } })) 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 - } - - codeVerifier := oauth2.SetAuthURLParam("code_verifier", verifier) + //AuthCodeOption can be overriden from the config file. + opts := make([]oauth2.AuthCodeOption, 0) codeChallenge := oauth2.SetAuthURLParam("code_challenge", verifierCode) - challengeMethod := oauth2.SetAuthURLParam("code_challenge_method", "S256") + codeChallengeMethod := oauth2.SetAuthURLParam("code_challenge_method", "S256") + opts = append(opts, codeChallenge, codeChallengeMethod) - 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:") + if _, found := auth.OAuth2.AuthCodeOptions["access_type"]; !found { + auth.OAuth2.AuthCodeOptions["access_type"] = "offline" + } + if _, found := auth.OAuth2.AuthCodeOptions["prompt"]; !found { + auth.OAuth2.AuthCodeOptions["prompt"] = "consent" + } + + //Add all the authCodeOptions from config. + for key, element := range auth.OAuth2.AuthCodeOptions { + opts = append(opts, oauth2.SetAuthURLParam(key, element)) + } + authURL := config.AuthCodeURL("state-token", opts...) - newToken, err = config.Exchange(context.Background(), code, codeVerifier) + err = open(authURL) if err != nil { - return false, err + output(fmt.Sprintf("Navigate to %s and authenticate", authURL)) } + <-done } OAuth2.CachedToken = newToken err = login(httpClient, endpoint, newToken.AccessToken) @@ -499,11 +522,15 @@ func login(httpClient *http.Client, endpoint string, accessToken string) error { return err } loginReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) - - _, err = httpClient.Do(loginReq) // Login to establish session. + resp, err := httpClient.Do(loginReq) // Login to establish session. if err != nil { - return errors.New("login failed") + return errors.Errorf("login failed %s", err) } + + if resp.StatusCode != 200 { + return errors.Errorf("login failed %s", resp.Status) + } + return nil } @@ -599,3 +626,21 @@ func securePrompt(output func(string), inputMsg string) string { secret := string(byteSecret) return strings.TrimSpace(secret) } + +// open opens the specified URL in the default browser of the user. +func open(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} diff --git a/config/auth/oauth2/config.go b/config/auth/oauth2/config.go index 0c18be83..0bcaf1f6 100644 --- a/config/auth/oauth2/config.go +++ b/config/auth/oauth2/config.go @@ -21,12 +21,13 @@ import ( // Config is the configuration for using OAuth2.0 to // authenticate with Spinnaker type Config struct { - TokenUrl string `yaml:"tokenUrl"` - AuthUrl string `yaml:"authUrl"` - ClientId string `yaml:"clientId"` - ClientSecret string `yaml:"clientSecret"` - Scopes []string `yaml:"scopes"` - CachedToken *oauth2.Token `yaml:"cachedToken,omitempty"` + TokenUrl string `yaml:"tokenUrl"` + AuthUrl string `yaml:"authUrl"` + ClientId string `yaml:"clientId"` + ClientSecret string `yaml:"clientSecret"` + AuthCodeOptions map[string]string `yaml:"authCodeOptions,omitempty"` + Scopes []string `yaml:"scopes"` + CachedToken *oauth2.Token `yaml:"cachedToken,omitempty"` } func (x *Config) IsValid() bool { diff --git a/config/example.yaml b/config/example.yaml index 8945032e..42a3d4e7 100644 --- a/config/example.yaml +++ b/config/example.yaml @@ -35,6 +35,11 @@ auth: scopes: - scope1 - scope2 + # To set Oauth AuthCodeOptions, follow the following format. Auzre Oauth2 API needs this + # see https://pkg.go.dev/golang.org/x/oauth2#AuthCodeOption + AuthCodeOptions: + access_type: online + prompt: none # To set a cached token, follow the following format: # note that these yaml keys must match the golang struct tags exactly because of yaml.UnmarshalStrict