Repository: Versent/saml2aws
Pull request #321 (2019-07-02 20:49:29)

Try alternate Google 2FA method if primary isn't supported
Implements #305

Workaround for #238, #311


Patch:
diff --git a/pkg/provider/googleapps/googleapps.go b/pkg/provider/googleapps/googleapps.go
index 19d7b39..af8ae2a 100644
--- a/pkg/provider/googleapps/googleapps.go
+++ b/pkg/provider/googleapps/googleapps.go
@@ -270,13 +270,82 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u
 			return kc.loadResponsePage(u.String(), submitURL, responseForm)
 		}
 
-		return nil, errors.Errorf("unsupported second factor: %s", secondActionURL)
+		skipResponseForm, skipActionURL, err := extractInputsByFormQuery(doc, `[action$="skip"]`)
+		if err != nil {
+			return nil, errors.Wrap(err, "unable to extract skip form")
+		}
+
+		if skipActionURL == "" {
+			return nil, errors.Errorf("unsupported second factor: %s", secondActionURL)
+		}
+
+		u.Path = skipActionURL
+
+		return kc.loadAlternateChallengePage(u.String(), submitURL, skipResponseForm)
+
 	}
 
 	return doc, nil
 
 }
 
+func (kc *Client) loadAlternateChallengePage(submitURL string, referer string, authForm url.Values) (*goquery.Document, error) {
+
+	req, err := http.NewRequest("POST", submitURL, strings.NewReader(authForm.Encode()))
+	if err != nil {
+		return nil, errors.Wrap(err, "error retrieving login form")
+	}
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("Accept-Language", "en-US")
+	req.Header.Set("Content-Language", "en-US")
+	req.Header.Set("Referer", referer)
+
+	res, err := kc.client.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to make request to login form")
+	}
+
+	doc, err := goquery.NewDocumentFromReader(res.Body)
+	if err != nil {
+		return nil, errors.Wrap(err, "error parsing login page html document")
+	}
+
+	var challengeEntry string
+
+	doc.Find("form[data-challengeentry]").EachWithBreak(func(i int, s *goquery.Selection) bool {
+		action, ok := s.Attr("action")
+		if !ok {
+			return true
+		}
+
+		if strings.Contains(action, "challenge/totp/") ||
+			strings.Contains(action, "challenge/ipp/") ||
+			strings.Contains(action, "challenge/az/") {
+
+			challengeEntry, _ = s.Attr("data-challengeentry")
+			return false
+		}
+
+		return true
+	})
+
+	if challengeEntry == "" {
+		return nil, errors.New("unable to find supported second factor")
+	}
+
+	query := fmt.Sprintf(`[data-challengeentry="%s"]`, challengeEntry)
+	responseForm, newActionURL, err := extractInputsByFormQuery(doc, query)
+	if err != nil {
+		return nil, errors.Wrap(err, "unable to extract challenge form")
+	}
+
+	u, _ := url.Parse(submitURL)
+	u.Path = newActionURL
+
+	return kc.loadChallengePage(u.String(), submitURL, responseForm)
+}
+
 func (kc *Client) postJSON(submitURL string, values map[string]string, referer string) (*http.Response, error) {
 
 	data, _ := json.Marshal(values)
@@ -351,10 +420,14 @@ func mustFindErrorMsg(doc *goquery.Document) string {
 }
 
 func extractInputsByFormID(doc *goquery.Document, formID string) (url.Values, string, error) {
+	return extractInputsByFormQuery(doc, fmt.Sprintf("#%s", formID))
+}
+
+func extractInputsByFormQuery(doc *goquery.Document, formQuery string) (url.Values, string, error) {
 	formData := url.Values{}
 	var actionURL string
 
-	query := fmt.Sprintf("form#%s", formID)
+	query := fmt.Sprintf("form%s", formQuery)
 
 	//get action url
 	doc.Find(query).Each(func(i int, s *goquery.Selection) {
@@ -365,7 +438,7 @@ func extractInputsByFormID(doc *goquery.Document, formID string) (url.Values, st
 		actionURL = action
 	})
 
-	query = fmt.Sprintf("form#%s", formID)
+	query = fmt.Sprintf("form%s", formQuery)
 
 	// extract form data to passthrough
 	doc.Find(query).Find("input").Each(func(i int, s *goquery.Selection) {


Test patch:
diff --git a/pkg/provider/googleapps/googleapps_test.go b/pkg/provider/googleapps/googleapps_test.go
index 3bd892b..c9c9bdc 100644
--- a/pkg/provider/googleapps/googleapps_test.go
+++ b/pkg/provider/googleapps/googleapps_test.go
@@ -24,16 +24,21 @@ func TestExtractInputByName(t *testing.T) {
 	require.Equal(t, "test error message", captcha)
 }
 
-func TestExtractInputsByFormID(t *testing.T) {
+func TestExtractInputsByFormQuery(t *testing.T) {
 	html := `<html><body><form id="dev" action="http://example.com/test"><input name="pass" value="test error message"\></form></body></html>`
 
 	doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
 	require.Nil(t, err)
 
-	form, actionURL, err := extractInputsByFormID(doc, "dev")
+	form, actionURL, err := extractInputsByFormQuery(doc, "#dev")
 	require.Nil(t, err)
 	require.Equal(t, "http://example.com/test", actionURL)
 	require.Equal(t, "test error message", form.Get("pass"))
+
+	form2, actionURL2, err := extractInputsByFormQuery(doc, `[action$="/test"]`)
+	require.Nil(t, err)
+	require.Equal(t, "http://example.com/test", actionURL2)
+	require.Equal(t, "test error message", form2.Get("pass"))
 }
 func TestExtractErrorMsg(t *testing.T) {
 	html := `<html><body><span class="error-msg">test error message</span></body></html>`