diff --git a/.gitignore b/.gitignore index 186524f9f..b7bbf8893 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ bin/ # direnv .envrc + +saml2aws.iml diff --git a/pkg/page/form.go b/pkg/page/form.go index cdbb6e0be..65884a7d3 100644 --- a/pkg/page/form.go +++ b/pkg/page/form.go @@ -46,6 +46,7 @@ func NewFormFromDocument(doc *goquery.Document, formFilter string) (*Form, error if formFilter == "" { formFilter = "form[action]" } + formSelection := doc.Find(formFilter).First() if formSelection.Size() != 1 { return nil, fmt.Errorf("could not find form") diff --git a/pkg/provider/okta/okta.go b/pkg/provider/okta/okta.go index 7361e31d4..ab19406d8 100644 --- a/pkg/provider/okta/okta.go +++ b/pkg/provider/okta/okta.go @@ -85,6 +85,12 @@ type mfaChallengeContext struct { challengeResponseBody string } +// mfaOption store the mfa position in response and mfa description +type mfaOption struct { + position int + mfaString string +} + // New creates a new Okta client func New(idpAccount *cfg.IDPAccount) (*Client, error) { @@ -115,9 +121,7 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) { type ctxKey string -// Authenticate logs into Okta and returns a SAML response -func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { - +func (oc *Client) InitSession(loginDetails *creds.LoginDetails) (string, error) { oktaURL, err := url.Parse(loginDetails.URL) if err != nil { return "", errors.Wrap(err, "error building oktaURL") @@ -179,10 +183,31 @@ func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) } } + return oktaSessionToken, err +} + +// Authenticate logs into Okta and returns a SAML response +// Legacy method that should not be used anymore +func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { + oktaSessionToken, err := oc.InitSession(loginDetails) + if err != nil { + return "", errors.Wrap(err, "error authenticating with okta") + } + + return oc.AuthenticateWithService(oktaSessionToken, loginDetails) +} + +func (oc *Client) AuthenticateWithService(oktaSessionToken string, loginDetails *creds.LoginDetails) (string, error) { + oktaURL, err := url.Parse(loginDetails.URL) + if err != nil { + return "", errors.Wrap(err, "error building oktaURL") + } + + oktaOrgHost := oktaURL.Host //now call saml endpoint oktaSessionRedirectURL := fmt.Sprintf("https://%s/login/sessionCookieRedirect", oktaOrgHost) - req, err = http.NewRequest("GET", oktaSessionRedirectURL, nil) + req, err := http.NewRequest("GET", oktaSessionRedirectURL, nil) if err != nil { return "", errors.Wrap(err, "error building authentication request") } @@ -209,7 +234,16 @@ func (oc *Client) follow(ctx context.Context, req *http.Request, loginDetails *c var handler func(context.Context, *goquery.Document) (context.Context, *http.Request, error) - if docIsFormRedirectToTarget(doc, oc.targetURL) { + if pageIsJfrog(res) { + logger.WithField("type", "saml-response-to-jfrog").Debug("doc detect") + + var apiKey, userName, err = oc.fetchArtifactoryAuth() + if err != nil { + return "", err + } + return userName + ":" + apiKey, nil + + } else if docIsFormRedirectToTarget(doc, oc.targetURL) { logger.WithField("type", "saml-response-to-aws").Debug("doc detect") if samlResponse, ok := extractSAMLResponse(doc); ok { decodedSamlResponse, err := base64.StdEncoding.DecodeString(samlResponse) @@ -222,12 +256,12 @@ func (oc *Client) follow(ctx context.Context, req *http.Request, loginDetails *c } else if docIsFormSamlRequest(doc) { logger.WithField("type", "saml-request").Debug("doc detect") handler = oc.handleFormRedirect - } else if docIsFormResume(doc) { - logger.WithField("type", "resume").Debug("doc detect") - handler = oc.handleFormRedirect } else if docIsFormSamlResponse(doc) { logger.WithField("type", "saml-response").Debug("doc detect") handler = oc.handleFormRedirect + } else if docIsFormResume(doc) { + logger.WithField("type", "resume").Debug("doc detect") + handler = oc.handleFormRedirect } else { req, err = http.NewRequest("GET", loginDetails.URL, nil) if err != nil { @@ -242,6 +276,7 @@ func (oc *Client) follow(ctx context.Context, req *http.Request, loginDetails *c return "", errors.Wrap(err, "error retrieving body from response") } stateToken, err := getStateTokenFromOktaPageBody(string(body)) + if err != nil { return "", errors.Wrap(err, "error retrieving saml response") } @@ -272,10 +307,11 @@ func getStateTokenFromOktaPageBody(responseBody string) (string, error) { return strings.Replace(match[1], `\x2D`, "-", -1), nil } -func parseMfaIdentifer(json string, arrayPosition int) string { +func parseMfaIdentifer(json string, arrayPosition int) (string, string) { mfaProvider := gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.provider", arrayPosition)).String() factorType := strings.ToUpper(gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.factorType", arrayPosition)).String()) - return fmt.Sprintf("%s %s", mfaProvider, factorType) + profile := gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.profile", arrayPosition)).String() + return fmt.Sprintf("%s %s", mfaProvider, factorType), fmt.Sprintf("%s %s -- %s", mfaProvider, factorType, profile) } func (oc *Client) handleFormRedirect(ctx context.Context, doc *goquery.Document) (context.Context, *http.Request, error) { @@ -287,6 +323,10 @@ func (oc *Client) handleFormRedirect(ctx context.Context, doc *goquery.Document) return ctx, req, err } +func pageIsJfrog(res *http.Response) bool { + return res.Request.URL.String() == "https://sonder.jfrog.io/ui/login/" +} + func docIsFormSamlRequest(doc *goquery.Document) bool { return doc.Find("input[name=\"SAMLRequest\"]").Size() == 1 } @@ -323,6 +363,84 @@ func extractSAMLResponse(doc *goquery.Document) (v string, ok bool) { return doc.Find("input[name=\"SAMLResponse\"]").Attr("value") } +func (oc *Client) fetchArtifactoryAuth() (string, string, error) { + // Fetch the current user email, needed for the subsequent requests + req, err := http.NewRequest("GET", "https://sonder.jfrog.io/ui/api/v1/ui/auth/current", nil) + req.Header.Set("X-Requested-With", "XMLHttpRequest") + + if err != nil { + return "", "", err + } + + res, err := oc.client.Do(req) + if err != nil { + return "", "", err + } + var body struct { + Name string `json:"name"` + } + err = json.NewDecoder(res.Body).Decode(&body) + if err != nil { + return "", "", err + } + + logger.WithField("user", body.Name).Debug("Authenticated as") + + // Force initializing the user profile to get a USER_PROFILE_TOKEN set in the cookies + var jsonData = []byte(`{}`) + req, _ = http.NewRequest("POST", "https://sonder.jfrog.io/ui/api/v1/ui/userProfile", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Requested-With", "XMLHttpRequest") + + _, err = oc.client.Do(req) + if err != nil { + logger.WithField("error", err).Debug("there was an error") + return "", "", err + } + + userApiKeyURL := "https://sonder.jfrog.io/ui/api/v1/ui/userApiKey/" + body.Name + // Attempt to get the api key from the call. + req, _ = http.NewRequest("GET", userApiKeyURL, nil) + + apiKey, _ := oc.getApiKey(req) + if apiKey != "" { + return apiKey, body.Name, nil + } + // Fallback and create the API key if not existing + req, _ = http.NewRequest("POST", userApiKeyURL, nil) + req.Header.Set("X-Requested-With", "XMLHttpRequest") + basicAuth := base64.StdEncoding.EncodeToString([]byte(body.Name + ":")) + req.Header.Set("X-JFrog-Reauthentication", "Basic "+basicAuth) + + apiKey, err = oc.getApiKey(req) + + if apiKey != "" { + return apiKey, body.Name, nil + } + return "", "", err +} + +func (oc *Client) getApiKey(req *http.Request) (string, error) { + var apiKeyBody struct { + ApiKey string `json:"apiKey,omitempty"` + } + res, err := oc.client.Do(req) + + if err != nil { + return "", err + } + + err = json.NewDecoder(res.Body).Decode(&apiKeyBody) + if err != nil { + return "", err + } + + if apiKeyBody.ApiKey != "" { + return apiKeyBody.ApiKey, nil + } + return "", err +} + func findMfaOption(mfa string, mfaOptions []string, startAtIdx int) int { for idx, val := range mfaOptions { if startAtIdx > idx { @@ -335,11 +453,23 @@ func findMfaOption(mfa string, mfaOptions []string, startAtIdx int) int { return 0 } +func findAllMatchingMFA(mfa string, mfaOptions []string) []mfaOption { + var matchingMfas []mfaOption + + for i, val := range mfaOptions { + if strings.Contains(strings.ToUpper(val), mfa) { + matchingMfas = append(matchingMfas, mfaOption{position: i, mfaString: val}) + } + + } + return matchingMfas +} + func getMfaChallengeContext(oc *Client, mfaOption int, resp string) (*mfaChallengeContext, error) { stateToken := gjson.Get(resp, "stateToken").String() factorID := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d.id", mfaOption)).String() oktaVerify := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d._links.verify.href", mfaOption)).String() - mfaIdentifer := parseMfaIdentifer(resp, mfaOption) + mfaIdentifer, _ := parseMfaIdentifer(resp, mfaOption) logger.WithField("factorID", factorID).WithField("oktaVerify", oktaVerify).WithField("mfaIdentifer", mfaIdentifer).Debug("MFA") @@ -398,17 +528,27 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, // choose an mfa option if there are multiple enabled mfaOption := 0 var mfaOptions []string + var profiles []string for i := range gjson.Get(resp, "_embedded.factors").Array() { - identifier := parseMfaIdentifer(resp, i) + identifier, profile := parseMfaIdentifer(resp, i) if val, ok := supportedMfaOptions[identifier]; ok { mfaOptions = append(mfaOptions, val) } else { mfaOptions = append(mfaOptions, "UNSUPPORTED: "+identifier) } + profiles = append(profiles, profile) } if strings.ToUpper(oc.mfa) != "AUTO" { - mfaOption = findMfaOption(oc.mfa, mfaOptions, 0) + allMatchingMfaOptions := findAllMatchingMFA(oc.mfa, profiles) + if len(allMatchingMfaOptions) > 1 { + pickedOption := prompter.Choose("Select which MFA option to use", getMfaStringArr(allMatchingMfaOptions)) + mfaOption = allMatchingMfaOptions[pickedOption].position + } else if len(allMatchingMfaOptions) == 1 { + mfaOption = allMatchingMfaOptions[0].position + } else { + return "", errors.New("No Matching MFA registered on OKTA. Here are your registered MFAs: " + fmt.Sprintf("%v\n", profiles)) + } } else if len(mfaOptions) > 1 { mfaOption = prompter.Choose("Select which MFA option to use", mfaOptions) } @@ -774,6 +914,14 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, return "", errors.New("no mfa options provided") } +func getMfaStringArr(options []mfaOption) []string { + var list []string + for _, option := range options { + list = append(list, option.mfaString) + } + return list +} + func fidoWebAuthn(oc *Client, oktaOrgHost string, challengeContext *mfaChallengeContext, mfaOption int, stateToken string, mfaOptions []string, resp string) (string, error) { var signedAssertion *SignedAssertion diff --git a/pkg/provider/okta/okta_webauthn.go b/pkg/provider/okta/okta_webauthn.go index e1c7de14e..3f2eb48d8 100644 --- a/pkg/provider/okta/okta_webauthn.go +++ b/pkg/provider/okta/okta_webauthn.go @@ -3,6 +3,7 @@ package okta import ( "errors" "fmt" + "strings" "time" "github.com/marshallbrekka/go-u2fhost" @@ -122,7 +123,15 @@ func (d *FidoClient) ChallengeU2F() (*SignedAssertion, error) { prompted = true } default: - return responsePayload, err + errString := fmt.Sprintf("%s", err) + if strings.Contains(errString, "U2FHIDError") { + fmt.Printf("Let's keep looping till times out. err: %s \n", err) + } else if strings.Contains(errString, "hidapi: hid_error is not implemented yet") { + fmt.Printf("Let's keep looping till times out. err: %s \n", err) + } else { + fmt.Printf("other errors? err: %s \n", err) + return responsePayload, err + } } } }