Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

catching other hid device errors and prompt for FIDO auth selections #668

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ bin/

# direnv
.envrc

saml2aws.iml
1 change: 1 addition & 0 deletions pkg/page/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
174 changes: 161 additions & 13 deletions pkg/provider/okta/okta.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ type mfaChallengeContext struct {
challengeResponseBody string
}

// mfaOption store the mfa position in response and mfa description
type mfaOption struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hinling-sonder WDYT about extracting the profile.authenticatorName from the json and add it to this struct to have a human-readable MFA option in the prompt? I found it really helpful to have this instead of the full profile field.

position int
mfaString string
}

// New creates a new Okta client
func New(idpAccount *cfg.IDPAccount) (*Client, error) {

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
}
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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")
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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")

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion pkg/provider/okta/okta_webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package okta
import (
"errors"
"fmt"
"strings"
"time"

"github.com/marshallbrekka/go-u2fhost"
Expand Down Expand Up @@ -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
}
}
}
}
Expand Down