diff --git a/pkg/provider/okta/okta.go b/pkg/provider/okta/okta.go index c69d44b5d..65978ca50 100644 --- a/pkg/provider/okta/okta.go +++ b/pkg/provider/okta/okta.go @@ -975,7 +975,8 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, duoMfaOptions = append(duoMfaOptions, "Duo Push") } - if doc.Find("option[value=\"token\"]").Length() > 0 { + if doc.Find("option[value=\"token\"]").Length() > 0 || + doc.Find("option[value=\"phone1\"]").Length() > 0 { duoMfaOptions = append(duoMfaOptions, "Passcode") } diff --git a/pkg/provider/okta/okta_test.go b/pkg/provider/okta/okta_test.go index a45b75d8f..c55499f61 100644 --- a/pkg/provider/okta/okta_test.go +++ b/pkg/provider/okta/okta_test.go @@ -18,8 +18,10 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" + "github.com/versent/saml2aws/v2/mocks" "github.com/versent/saml2aws/v2/pkg/cfg" "github.com/versent/saml2aws/v2/pkg/creds" + "github.com/versent/saml2aws/v2/pkg/prompter" "github.com/versent/saml2aws/v2/pkg/provider" ) @@ -266,8 +268,107 @@ func TestVerifyMfa(t *testing.T) { } func TestVerifyMfa_Duo(t *testing.T) { + t.Run("Duo Push", func(t *testing.T) { + ts := setupTestDuoHttpServer(t, "Duo Push") + defer ts.Close() + oc, loginDetails := setupTestClient(t, ts, "DUO") + + err := oc.setDeviceTokenCookie(loginDetails) + assert.Nil(t, err) + + var out bytes.Buffer + log.SetOutput(&out) + context, err := verifyMfa(oc, "host-from-argument", &creds.LoginDetails{DuoMFAOption: "Duo Push"}, fmt.Sprintf(`{ + "stateToken": "TOKEN_1", + "status": "MFA_REQUIRED", + "_embedded": { + "factors": [ + { + "id": "factor_id", + "provider": "DUO", + "factorType": "web", + "_links": { + "verify": { + "href": "%s/verify", + "hints": { + "allow": [ + "POST" + ] + } + } + } + } + ] + } + }`, ts.URL)) + log.SetOutput(os.Stderr) + assert.Nil(t, err) + assert.Equal(t, "session-token-fffffff", context) + }) + + t.Run("Passcode", func(t *testing.T) { + ts := setupTestDuoHttpServer(t, "Passcode") + defer ts.Close() + oc, loginDetails := setupTestClient(t, ts, "DUO") + + err := oc.setDeviceTokenCookie(loginDetails) + assert.Nil(t, err) + + pr := &mocks.Prompter{} + prompter.SetPrompter(pr) + pr.Mock.On("StringRequired", "Enter passcode").Return("000000") + + var out bytes.Buffer + log.SetOutput(&out) + context, err := verifyMfa(oc, "host-from-argument", &creds.LoginDetails{DuoMFAOption: "Passcode"}, fmt.Sprintf(`{ + "stateToken": "TOKEN_1", + "status": "MFA_REQUIRED", + "_embedded": { + "factors": [ + { + "id": "factor_id", + "provider": "DUO", + "factorType": "web", + "_links": { + "verify": { + "href": "%s/verify", + "hints": { + "allow": [ + "POST" + ] + } + } + } + } + ] + } + }`, ts.URL)) + log.SetOutput(os.Stderr) + assert.Nil(t, err) + assert.Equal(t, "session-token-fffffff", context) + }) +} + +func setupTestClient(t *testing.T, ts *httptest.Server, mfa string) (*Client, *creds.LoginDetails) { + testTransport := http.DefaultTransport.(*http.Transport).Clone() + testTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + opts := &provider.HTTPClientOptions{IsWithRetries: false} + client, _ := provider.NewHTTPClient(testTransport, opts) + ac := &Client{ + client: client, + targetURL: ts.URL, + mfa: mfa, + disableSessions: false, + rememberDevice: true, + } + loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "user@example.com", Password: "test123"} + return ac, loginDetails +} + +func setupTestDuoHttpServer(t *testing.T, duoFactor string) *httptest.Server { verifyCounter := 0 statusCounter := 0 + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/frame/web/v1/auth": @@ -323,12 +424,25 @@ func TestVerifyMfa_Duo(t *testing.T) { assert.Nil(t, err) query, err = url.ParseQuery(string(body)) - assert.Equal(t, url.Values{ - "device": {"phone1"}, - "factor": {"Duo Push"}, - "out_of_date": {"false"}, - "sid": {"secret_sid"}, - }, query) + + switch duoFactor { + case "Duo Push": + assert.Equal(t, url.Values{ + "device": {"phone1"}, + "factor": {"Duo Push"}, + "out_of_date": {"false"}, + "sid": {"secret_sid"}, + }, query) + case "Passcode": + assert.Equal(t, url.Values{ + "device": {"phone1"}, + "factor": {"Passcode"}, + "passcode": {"000000"}, + "out_of_date": {"false"}, + "sid": {"secret_sid"}, + }, query) + } + assert.Nil(t, err) _, err = w.Write([]byte(`{"stat": "OK", "response": {"txid": "txid_1234"}}`)) @@ -482,59 +596,8 @@ func TestVerifyMfa_Duo(t *testing.T) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } })) - defer ts.Close() - - t.Run("Duo", func(t *testing.T) { - oc, loginDetails := setupTestClient(t, ts, "DUO") - - err := oc.setDeviceTokenCookie(loginDetails) - assert.Nil(t, err) - - var out bytes.Buffer - log.SetOutput(&out) - context, err := verifyMfa(oc, "host-from-argument", &creds.LoginDetails{DuoMFAOption: "Duo Push"}, fmt.Sprintf(`{ - "stateToken": "TOKEN_1", - "status": "MFA_REQUIRED", - "_embedded": { - "factors": [ - { - "id": "factor_id", - "provider": "DUO", - "factorType": "web", - "_links": { - "verify": { - "href": "%s/verify", - "hints": { - "allow": [ - "POST" - ] - } - } - } - } - ] - } - }`, ts.URL)) - log.SetOutput(os.Stderr) - assert.Nil(t, err) - assert.Equal(t, "session-token-fffffff", context) - }) -} -func setupTestClient(t *testing.T, ts *httptest.Server, mfa string) (*Client, *creds.LoginDetails) { - testTransport := http.DefaultTransport.(*http.Transport).Clone() - testTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - opts := &provider.HTTPClientOptions{IsWithRetries: false} - client, _ := provider.NewHTTPClient(testTransport, opts) - ac := &Client{ - client: client, - targetURL: ts.URL, - mfa: mfa, - disableSessions: false, - rememberDevice: true, - } - loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "user@example.com", Password: "test123"} - return ac, loginDetails + return ts } func TestSetDeviceTokenCookie(t *testing.T) {