Repository: Versent/saml2aws
Pull request #158 (2018-04-02 00:28:30)

fix(PingFed) Resolved issue with Ping MFA changing page.
* Updated AWS SDK to 1.13.25


Patch:
diff --git a/Gopkg.lock b/Gopkg.lock
index 4dec8c9..5894615 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -59,6 +59,7 @@
     "aws/request",
     "aws/session",
     "aws/signer/v4",
+    "internal/sdkio",
     "internal/sdkrand",
     "internal/shareddefaults",
     "private/protocol",
@@ -68,8 +69,8 @@
     "private/protocol/xml/xmlutil",
     "service/sts"
   ]
-  revision = "bafcd9ccc717e9bc5406acaea370577299223873"
-  version = "v1.13.11"
+  revision = "74ca63959be8b9a9a24cd86eaaea8bf99de8022d"
+  version = "v1.13.25"
 
 [[projects]]
   name = "github.com/beevik/etree"
@@ -222,6 +223,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "d43d69c82e9d60c1db79017936bd7b42eba3c6db89885d8380071a79897b4c5f"
+  inputs-digest = "8e5ad434dff4da641ffdfd4d83073bdac313a6290f29590e4ce41e0061778c89"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
index b6e9292..c0faa5c 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -39,7 +39,7 @@
 
 [[constraint]]
   name = "github.com/aws/aws-sdk-go"
-  version = "1.13.11"
+  version = "1.13.25"
 
 [[constraint]]
   name = "github.com/beevik/etree"
diff --git a/pkg/provider/pingfed/example/mfapage.html b/pkg/provider/pingfed/example/mfapage.html
new file mode 100644
index 0000000..1510118
--- /dev/null
+++ b/pkg/provider/pingfed/example/mfapage.html
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+<!DOCTYPE html>
+<html>
+<head>
+	<title></title>
+	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+	<meta name = "format-detection" content = "telephone=no">
+	<meta http-equiv="x-ua-compatible" content="IE=edge" >
+	<link rel="stylesheet" href="/pingid/assets/css/main-v21.125.css" media="screen" title="no title" charset="utf-8">
+	<link rel="stylesheet" media="screen" type="text/css" href="/pingid/assets/css/jsdisabled.css" />
+    <script type="text/javascript" src="/pingid/assets/js/jquery-1.11.1.min.js"></script>
+    <script type="text/javascript" src="/pingid/assets/js/spin.js"></script> 
+</head>
+<body>
+	<noscript>
+    
+
+
+
+
+
+
+
+
+<!DOCTYPE html>
+<html>
+<head>
+	<title></title>
+	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+	<meta name = "format-detection" content = "telephone=no">
+	<link rel="stylesheet" href="/pingid/assets/css/jsdisabled.css" media="screen" title="no title" charset="utf-8">
+</head>
+<body>
+    <div class="nojspage">
+            
+
+            <div class="window error">
+                <div class="content">
+                    <div class="status"></div>
+        			<div class="title-text">
+        			    Important
+                    </div>
+
+        	            <div class="error-text">
+        					<div class="text">
+        					    PingID requires Javascript to be enabled. If the problem persists, please contact your administrator.
+        					</div>
+        	            </div>
+
+                </div>
+            </div>
+
+
+            <div class="footer">
+                <div class="pingid_logo"></div>
+                <div class="copyright">
+                    Copyright &copy; 2003-2018 Ping Identity Corporation. All rights reserved.
+                </div>
+            </div>
+    </div>
+</body>
+</html>
+	   <style type="text/css">
+			.dialog { display:none; }
+		</style>
+	</noscript>
+    <div class="dialog">
+       		
+    
+    
+    
+
+    <div class="window authenticating single-device">
+        <div class="content">
+            <div class="status">
+                <div id="spinner"></div>
+            </div>
+
+            
+                
+                
+                    <div class="text">
+                        Authenticating
+                    </div>
+                
+            
+
+            
+            
+        </div>
+    </div>
+
+    <div class="admin-message">Please contact VMS support@example.com.au</div>
+
+    <div class="footer">
+        
+        <div class="logo"></div>
+        <div class="copyright">
+            Copyright &copy; 2003-2018 Ping Identity Corporation. All rights reserved.
+        </div>
+    </div>
+
+    <!-- This can be removed once Async FF is removed. -->
+    <form method="POST" action="https://authenticator.pingone.com/pingid/ppm/auth/poll" id="form1">
+        <input type="hidden" name="csrfToken" id="csrfToken" value="fc80998c-34d8-4dd2-925c-3b3be8a0dee8" encode="false" />
+        <noscript><input type="submit" value="Resume"/></noscript>
+    </form>
+
+    <form method="GET" action="" id="reponseView">
+        <input type="hidden" name="csrfToken" id="csrfToken" value="fc80998c-34d8-4dd2-925c-3b3be8a0dee8" encode="false" />
+        <noscript><input type="submit" value="Resume"/></noscript>
+    </form>
+
+    <form method="GET" action="" id="errorReponseView">
+        <input type="hidden" name="csrfToken" id="csrfToken" value="fc80998c-34d8-4dd2-925c-3b3be8a0dee8" encode="false" />
+    </form>
+
+    <div id="authModelSection">
+        <input type="hidden" name="isAsync" id="isAsync" value="false" encode="false" />
+        <input type="hidden" name="actionLink" id="actionLink" value="https://authenticator.pingone.com/pingid/ppm/auth/poll" encode="false" />
+        <input type="hidden" name="useCodeUrl" id="useCodeUrl" value="/pingid/ppm/auth/usecode" encode="false" />
+    </div>
+
+	<script type="text/javascript" src="/pingid/assets/js/utils/spinner.js"></script>
+    <script type="text/javascript" src="/pingid/assets/js/utils/getAuthStatus.js"></script>
+    <script type="text/javascript" src="/pingid/assets/js/utils/authenticationselfsubmit.js"></script>
+    <script type="text/javascript" src="/pingid/assets/js/utils/usecodesubmit.js"></script>
+
+    </div>
+</body>
+</html>
diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go
index 0a363da..8b1c03e 100644
--- a/pkg/provider/pingfed/pingfed.go
+++ b/pkg/provider/pingfed/pingfed.go
@@ -20,11 +20,11 @@ var logger = logrus.WithField("provider", "pingfed")
 
 // Client wrapper around PingFed + PingId enabling authentication and retrieval of assertions
 type Client struct {
-	client        *provider.HTTPClient
-	idpAccount    *cfg.IDPAccount
-	authSubmitURL string
-	samlAssertion string
-	mfaRequired   bool
+	client     *provider.HTTPClient
+	idpAccount *cfg.IDPAccount
+	//authSubmitURL string
+	//samlAssertion string
+	//mfaRequired   bool
 }
 
 // New create a new PingFed client
@@ -41,76 +41,47 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 	client.DisableFollowRedirect()
 
 	return &Client{
-		client:      client,
-		idpAccount:  idpAccount,
-		mfaRequired: false,
+		client:     client,
+		idpAccount: idpAccount,
+		//mfaRequired: false,
 	}, nil
 }
 
 // Authenticate Authenticate to PingFed and return the data from the body of the SAML assertion.
 func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
 
-	authForm := url.Values{}
-
-	pingFedURL := fmt.Sprintf("%s/idp/startSSO.ping?PartnerSpId=%s", loginDetails.URL, ac.idpAccount.AmazonWebservicesURN)
-
-	logger.WithField("url", pingFedURL).Debug("GET")
-
-	res, err := ac.client.Get(pingFedURL)
-	if err != nil {
-		return "", errors.Wrap(err, "error retieving form")
-	}
-
-	doc, err := goquery.NewDocumentFromResponse(res)
+	authSubmitURL, authForm, err := ac.getLoginForm(loginDetails)
 	if err != nil {
-		return "", errors.Wrap(err, "failed to build document from response")
-	}
-
-	doc.Find("input").Each(func(i int, s *goquery.Selection) {
-		updateLoginFormData(authForm, s, loginDetails)
-	})
-
-	doc.Find("form").Each(func(i int, s *goquery.Selection) {
-		action, ok := s.Attr("action")
-		if !ok {
-			return
-		}
-		ac.authSubmitURL = action
-	})
-
-	if ac.authSubmitURL == "" {
-		return "", fmt.Errorf("unable to locate IDP authentication form submit URL")
+		return "", errors.Wrap(err, "error retrieving login form")
 	}
 
-	ac.authSubmitURL = fmt.Sprintf("%s%s", loginDetails.URL, ac.authSubmitURL)
-
-	//log.Printf("id authentication url: %s", authSubmitURL)
-
-	req, err := http.NewRequest("POST", ac.authSubmitURL, strings.NewReader(authForm.Encode()))
+	req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode()))
 	if err != nil {
 		return "", errors.Wrap(err, "error building authentication request")
 	}
 
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 
-	logger.WithField("authSubmitURL", ac.authSubmitURL).WithField("req", dump.RequestString(req)).Debug("POST")
+	logger.WithField("authSubmitURL", authSubmitURL).WithField("req", dump.RequestString(req)).Debug("POST")
 
-	res, err = ac.client.Do(req)
+	res, err := ac.client.Do(req)
 	if err != nil {
 		return "", errors.Wrap(err, "error retrieving login form")
 	}
 
-	logger.WithField("authSubmitURL", ac.authSubmitURL).WithField("res", dump.ResponseString(res)).Debug("POST")
+	logger.WithField("authSubmitURL", authSubmitURL).WithField("res", dump.ResponseString(res)).Debug("POST")
+
+	var mfaRequired bool
 
 	//check for redirect, this indicates PingOne MFA being used
 	if res.StatusCode == 302 {
-		ac.mfaRequired = true
+		mfaRequired = true
 	}
 
-	logger.WithField("mfaRequired", ac.mfaRequired).Debug("POST")
+	logger.WithField("mfaRequired", mfaRequired).Debug("POST")
 
 	//process mfa
-	if ac.mfaRequired {
+	if mfaRequired {
 
 		mfaURL, err := res.Location()
 		if err != nil {
@@ -141,16 +112,23 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 			return "", errors.Wrap(err, "error building mfa authentication request")
 		}
 
+		logger.WithField("actionURL", actionURL).WithField("req", dump.RequestString(req)).Debug("POST")
+
 		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 		res, err = ac.client.Do(req)
 		if err != nil {
-			return "", errors.Wrap(err, "error retieving mfa response")
+			return "", errors.Wrap(err, "error retrieving mfa response")
 		}
 
 		logger.WithField("actionURL", actionURL).WithField("res", dump.ResponseString(res)).Debug("POST")
 
+		doc, err := goquery.NewDocumentFromResponse(res)
+		if err != nil {
+			return "", errors.Wrap(err, "failed to build document from response")
+		}
+
 		//extract form action and csrf token
-		form, actionURL, err = extractFormData(res)
+		form, actionURL, err = extractMfaFormData(doc)
 		if err != nil {
 			return "", errors.Wrap(err, "error extracting authentication form")
 		}
@@ -229,21 +207,60 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 	}
 
 	//try to extract SAMLResponse
-	doc, err = goquery.NewDocumentFromResponse(res)
+	doc, err := goquery.NewDocumentFromResponse(res)
 	if err != nil {
 		return "", errors.Wrap(err, "error parsing document")
 	}
 
 	var ok bool
 
-	ac.samlAssertion, ok = doc.Find("input[name=\"SAMLResponse\"]").Attr("value")
+	samlAssertion, ok := doc.Find("input[name=\"SAMLResponse\"]").Attr("value")
 	if !ok {
 		return "", errors.Wrap(err, "unable to locate saml response")
 	}
 
-	logger.WithField("samlAssertion", ac.samlAssertion).Debug("SAMLResponse")
+	logger.WithField("samlAssertion", samlAssertion).Debug("SAMLResponse")
+
+	return samlAssertion, nil
+}
+
+func (ac *Client) getLoginForm(loginDetails *creds.LoginDetails) (string, url.Values, error) {
+
+	authForm := url.Values{}
+
+	pingFedURL := fmt.Sprintf("%s/idp/startSSO.ping?PartnerSpId=%s", loginDetails.URL, ac.idpAccount.AmazonWebservicesURN)
+
+	logger.WithField("url", pingFedURL).Debug("GET")
+
+	res, err := ac.client.Get(pingFedURL)
+	if err != nil {
+		return "", nil, errors.Wrap(err, "error retieving form")
+	}
+
+	doc, err := goquery.NewDocumentFromResponse(res)
+	if err != nil {
+		return "", nil, errors.Wrap(err, "failed to build document from response")
+	}
+
+	doc.Find("input").Each(func(i int, s *goquery.Selection) {
+		updateLoginFormData(authForm, s, loginDetails)
+	})
+
+	authSubmitURL := ""
 
-	return ac.samlAssertion, nil
+	doc.Find("form").Each(func(i int, s *goquery.Selection) {
+		action, ok := s.Attr("action")
+		if !ok {
+			return
+		}
+		authSubmitURL = action
+	})
+
+	if authSubmitURL == "" {
+		return "", nil, fmt.Errorf("unable to locate IDP authentication form submit URL")
+	}
+
+	return fmt.Sprintf("%s%s", loginDetails.URL, authSubmitURL), authForm, nil
 }
 
 func updateLoginFormData(authForm url.Values, s *goquery.Selection, user *creds.LoginDetails) {
@@ -285,7 +302,7 @@ func extractFormData(res *http.Response) (url.Values, string, error) {
 		actionURL = action
 	})
 
-	// exxtract form data to passthrough
+	// extract form data to passthrough
 	doc.Find("input").Each(func(i int, s *goquery.Selection) {
 		name, ok := s.Attr("name")
 		if !ok {
@@ -300,3 +317,31 @@ func extractFormData(res *http.Response) (url.Values, string, error) {
 
 	return formData, actionURL, nil
 }
+
+func extractMfaFormData(doc *goquery.Document) (url.Values, string, error) {
+	formData := url.Values{}
+	var actionURL string
+	//get action url
+	doc.Find("#form1").Each(func(i int, s *goquery.Selection) {
+		action, ok := s.Attr("action")
+		if !ok {
+			return
+		}
+		actionURL = action
+	})
+
+	// extract form data to passthrough
+	doc.Find("#form1 > input").Each(func(i int, s *goquery.Selection) {
+		name, ok := s.Attr("name")
+		if !ok {
+			return
+		}
+		val, ok := s.Attr("value")
+		if !ok {
+			return
+		}
+		formData.Add(name, val)
+	})
+
+	return formData, actionURL, nil
+}


Test patch:
diff --git a/pkg/provider/pingfed/pingfed_test.go b/pkg/provider/pingfed/pingfed_test.go
new file mode 100644
index 0000000..73dea96
--- /dev/null
+++ b/pkg/provider/pingfed/pingfed_test.go
@@ -0,0 +1,24 @@
+package pingfed
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/url"
+	"testing"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/stretchr/testify/require"
+)
+
+func TestExtractMfaFormData(t *testing.T) {
+	data, err := ioutil.ReadFile("example/mfapage.html")
+	require.Nil(t, err)
+
+	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data))
+	require.Nil(t, err)
+
+	mfaForm, actionURL, err := extractMfaFormData(doc)
+	require.Nil(t, err)
+	require.Equal(t, "https://authenticator.pingone.com/pingid/ppm/auth/poll", actionURL)
+	require.Equal(t, url.Values{"csrfToken": []string{"fc80998c-34d8-4dd2-925c-3b3be8a0dee8"}}, mfaForm)
+}