Repository: Versent/saml2aws
Pull request #138 (2018-02-11 19:10:47)

Added support for RSA tokens with ADFS 2.x
This is a pretty unique situation in that the entire authentication flow is different between the default behaviour of the IDP and when it is configured to use RSA MFA tokens.

luckylittle: Love the branch name :-D

Patch:
diff --git a/helper/osxkeychain/osxkeychain_darwin.go b/helper/osxkeychain/osxkeychain_darwin.go
index 7ed5b2c..0032f50 100644
--- a/helper/osxkeychain/osxkeychain_darwin.go
+++ b/helper/osxkeychain/osxkeychain_darwin.go
@@ -24,7 +24,7 @@ package osxkeychain
 
 /*
 #cgo CFLAGS: -x objective-c -mmacosx-version-min=10.10
-#cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.10
+#cgo LDFLAGS: -framework Security -framework Foundation
 
 #include "osxkeychain_darwin.h"
 #include <stdlib.h>
diff --git a/pkg/provider/adfs2/adfs2.go b/pkg/provider/adfs2/adfs2.go
index c94dd13..f1f2060 100644
--- a/pkg/provider/adfs2/adfs2.go
+++ b/pkg/provider/adfs2/adfs2.go
@@ -1,10 +1,7 @@
 package adfs2
 
 import (
-	"bytes"
 	"crypto/tls"
-	"fmt"
-	"io/ioutil"
 	"log"
 	"net/http"
 	"net/http/cookiejar"
@@ -13,16 +10,17 @@ import (
 
 	"github.com/Azure/go-ntlmssp"
 	"github.com/PuerkitoBio/goquery"
-	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
 	"github.com/versent/saml2aws/pkg/cfg"
 	"github.com/versent/saml2aws/pkg/creds"
 )
 
+var logger = logrus.WithField("provider", "adfs2")
+
 // Client client for adfs2
 type Client struct {
 	idpAccount *cfg.IDPAccount
-	transport  http.RoundTripper
-	jar        http.CookieJar
+	client     *http.Client
 }
 
 // New new adfs2 client with ntlmssp configured
@@ -41,46 +39,29 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 		return nil, err
 	}
 
+	client := &http.Client{
+		Transport: transport,
+		Jar:       jar,
+	}
+
 	return &Client{
-		transport:  transport,
+		client:     client,
 		idpAccount: idpAccount,
-		jar:        jar,
 	}, nil
 }
 
 // Authenticate authenticate the user using the supplied login details
 func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
-	var samlAssertion string
-	client := http.Client{
-		Transport: ac.transport,
-		Jar:       ac.jar,
-		CheckRedirect: func(req *http.Request, via []*http.Request) error {
-			req.SetBasicAuth(loginDetails.Username, loginDetails.Password)
-			return nil
-		},
-	}
-
-	url := fmt.Sprintf("%s/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=%s", loginDetails.URL, ac.idpAccount.AmazonWebservicesURN)
-	req, err := http.NewRequest("GET", url, nil)
-	if err != nil {
-		return samlAssertion, err
-	}
-	req.SetBasicAuth(loginDetails.Username, loginDetails.Password)
-
-	res, err := client.Do(req)
-	if err != nil {
-		return samlAssertion, errors.Wrap(err, "error retieving login form")
-	}
-
-	data, err := ioutil.ReadAll(res.Body)
-	if err != nil {
-		return samlAssertion, errors.Wrap(err, "error retieving body")
+	switch ac.idpAccount.MFA {
+	case "RSA":
+		return ac.authenticateRsa(loginDetails)
+	default:
+		return ac.authenticateNTLM(loginDetails) // this is chosen as the default to maintain compatibility with existing users
 	}
+}
 
-	doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(data))
-	if err != nil {
-		return samlAssertion, errors.Wrap(err, "error parsing document")
-	}
+func extractSamlAssertion(doc *goquery.Document) (string, error) {
+	var samlAssertion string
 
 	doc.Find("input").Each(func(i int, s *goquery.Selection) {
 		name, ok := s.Attr("name")
diff --git a/pkg/provider/adfs2/example/loginpage.html b/pkg/provider/adfs2/example/loginpage.html
new file mode 100644
index 0000000..5eb1c41
--- /dev/null
+++ b/pkg/provider/adfs2/example/loginpage.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html lang="en-AU">
+    <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=10.000">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+        
+        <meta http-equiv="cache-control" content="no-cache,no-store">
+        <meta http-equiv="pragma" content="no-cache">
+        <meta http-equiv="expires" content="-1">
+        <meta name="mswebdialog-title" content="Connecting to Example">
+
+        <title>Sign In</title>
+        
+    </head>
+    <body dir="ltr" class="body">
+    <div id="noScript" style="position: static; width: 100%; height: 100%; z-index: 100; display: none;">
+        <h1>JavaScript required</h1>
+        <p>JavaScript is required. This web browser does not support JavaScript or JavaScript in this web browser is not enabled.</p>
+        <p>To find out if your web browser supports JavaScript or to enable JavaScript, see web browser help.</p>
+    </div>
+    <script type="text/javascript" language="JavaScript">
+         document.getElementById("noScript").style.display = "none";
+    </script>
+    <div id="fullPage">
+        <div id="brandingWrapper" class="float">
+            <div id="branding" class="illustrationClass"></div>
+        </div>
+        <div id="contentWrapper" class="float">
+            <div id="content">
+                <div id="header">
+                    <img class="logoImage" src="./test_files/logo.png" alt="Example">
+                </div>
+                <div id="workArea">
+                    
+    <div id="authArea" class="groupMargin">
+        
+        
+    <div id="loginArea">        
+        <div id="loginMessage" class="groupMargin">Sign in with your organizational account</div>
+
+        <form method="post" id="loginForm" autocomplete="off" novalidate="novalidate" onkeypress="if (event &amp;&amp; event.keyCode == 13) Login.submitLoginRequest();" action="https://id.example.com/adfs/ls/idpinitiatedsignon">
+            <div id="error" class="fieldMargin error smallText" style="display: none;">
+                <label id="errorText" for=""></label>
+            </div>
+
+            <div id="formsAuthenticationArea">
+                <div id="userNameArea">
+                    <input id="userNameInput" name="UserName" type="email" value="" tabindex="1" class="text fullWidth" spellcheck="false" placeholder="someone@example.com" autocomplete="off">     
+                </div>
+
+                <div id="passwordArea">
+                     <input id="passwordInput" name="Password" type="password" tabindex="2" class="text fullWidth" placeholder="Password" autocomplete="off">                                   
+                </div>
+                <div id="kmsiArea" style="display:none">
+                    <input type="checkbox" name="Kmsi" id="kmsiInput" value="true" tabindex="3">
+                    <label for="kmsiInput">Keep me signed in</label>
+                </div>
+                <div id="submissionArea" class="submitMargin">
+                    <span id="submitButton" class="submit" tabindex="4" onkeypress="if (event &amp;&amp; event.keyCode == 32) Login.submitLoginRequest();" onclick="return Login.submitLoginRequest();">Sign in</span>
+                </div>
+            </div>
+            <input id="optionForms" type="hidden" name="AuthMethod" value="FormsAuthentication">
+        </form>
+
+             <div id="authOptions">
+        <form id="options" method="post" action="https://id.example.com/adfs/ls/idpinitiatedsignon">
+            <script type="text/javascript">
+                function SelectOption(option) {
+                    var i = document.getElementById('optionSelection');
+                    i.value = option;
+                    document.forms['options'].submit();
+                    return false;
+                }
+            </script>
+            <input id="optionSelection" type="hidden" name="AuthMethod">
+            <div class="groupMargin"></div>
+        </form>
+      </div>
+
+        <div id="introduction" class="groupMargin">
+                                 
+        </div>
+
+    </div>
+
+    </div>
+
+                </div>
+                <div id="footerPlaceholder"></div>
+            </div>
+            <div id="footer">
+                <div id="footerLinks" class="floatReverse">
+                     <div><span id="copyright">© 2013 Microsoft</span></div>
+                </div>
+            </div>
+        </div> 
+    </div>
+</body></html>
\ No newline at end of file
diff --git a/pkg/provider/adfs2/example/passcode.html b/pkg/provider/adfs2/example/passcode.html
new file mode 100644
index 0000000..f59a70c
--- /dev/null
+++ b/pkg/provider/adfs2/example/passcode.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html lang="en-US">
+    <head>
+        <meta http-equiv="X-UA-Compatible" content="IE=10.000"/>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+        <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
+        <meta http-equiv="cache-control" content="no-cache,no-store"/>
+        <meta http-equiv="pragma" content="no-cache"/>
+        <meta http-equiv="expires" content="-1"/>
+        <meta name='mswebdialog-title' content='Connecting to Example Australia'/>
+
+        <title>RSA SecurID Authentication</title>
+
+    </head>
+    <body dir="ltr" class="body">
+    <div id="noScript" style="position:static; width:100%; height:100%; z-index:100">
+        <h1>JavaScript required</h1>
+        <p>JavaScript is required. This web browser does not support JavaScript or JavaScript in this web browser is not enabled.</p>
+        <p>To find out if your web browser supports JavaScript or to enable JavaScript, see web browser help.</p>
+    </div>
+    <script type="text/javascript" language="JavaScript">
+         document.getElementById("noScript").style.display = "none";
+    </script>
+    <div id="fullPage">
+        <div id="brandingWrapper" class="float">
+            <div id="branding"></div>
+        </div>
+        <div id="contentWrapper" class="float">
+            <div id="content">
+                <div id="header">
+                    <img class="logoImage" src="/adfs/portal/logo/logo.png?id=89BF8C27F50232853DAE602B2545AD35B6627BDBA8861101880261B2E64CC0BD" alt="Example"/>
+                </div>
+                <div id="workArea">
+
+    <div id="authArea" class="groupMargin">
+        <div class='fieldMargin bigText'>Welcome EXAMPLE\test123</div><div class='groupMargin'>For security reasons, we require additional information to verify your account</div>
+        <div class='groupMargin'><div id="loginArea">
+  <form method="post" id="passcodeForm" autocomplete="off" onsubmit="return validateForm(this)">
+    <!-- These inputs are required by the presentation framework.  Do not modify or remove -->
+    <input id="authMethod" type="hidden" name="AuthMethod" value="SecurIDAuthentication" />
+    <input id="context" type="hidden" name="Context" value="&lt;EncryptedData Type=&quot;http://www.w3.org/2001/04/xmlenc#Content&quot; xmlns=&quot;http://www.w3.org/2001/04/xmlenc#&quot;&gt;&lt;EncryptionMethod Algorithm=&quot;http://www.w3.org/2001/04/xmlenc#aes256-cbc&quot; /&gt;&lt;KeyInfo xmlns=&quot;http://www.w3.org/2000/09/xmldsig#&quot;&gt;&lt;EncryptedKey xmlns=&quot;http://www.w3.org/2001/04/xmlenc#&quot;&gt;&lt;EncryptionMethod Algorithm=&quot;http://www.w3.org/2001/04/xmlenc#rsa-1_5&quot; /&gt;&lt;KeyInfo xmlns=&quot;http://www.w3.org/2000/09/xmldsig#&quot;&gt;&lt;KeyName&gt;A87E278695EAC336FF24CF047B3E6B5AC1E7510D&lt;/KeyName&gt;&lt;/KeyInfo&gt;&lt;CipherData&gt;&lt;CipherValue&gt;i2oZON1eIEibLY9OHLI6HSy1FE7KhQ62qeLd4M+WsjIJP4zSFu5Z9ltp1Yi0+hSrWwyFbQkh+PqkyD3s9Nxb8QbwksDbRFvEb6mLJD+Y1mje2bx6Vq5CLhYTtvseXa/7rulE3msfphHXMWW+vJs8NbIXmNS8FaQaXi95yM0B8xxcwdXUZNXr7+0nCIU3O+Wn7CL91ezAp+UyHyegogWNrnraigIeR8zHGE+9ZE2GFj+955/qVjUoaMMxfeVVhRPh8CKto4Q0W5FQRlVJ/iHiifQvqakBGvGLgSj4x4EBYBn8pyUgXDxhID16fpr6i1MV7c0v1NCRTvCCUJDHJ8Vq+w==&lt;/CipherValue&gt;&lt;/CipherData&gt;&lt;/EncryptedKey&gt;&lt;/KeyInfo&gt;&lt;CipherData&gt;&lt;CipherValue&gt;tCR22fIiAMlXPz0SRbqjsegTg5Pi8BQSH83ELDlmbcMVVm/7AY6udDrzFCTWN+eODjY9Nsr5X7N9xJk+QteVKwjW/WR6ATCD8bQOp8PfWAs7Ih4g/f6Ns/JD27Q7YFQZVbr6OdxRfRZf3syfrxr5Rkks1vC99WFiQEzCqsLVPnktLkiN+dC80TogUqPmS8Y1o9yityuHh7YC3jSWP3Y2falRE2iedjD0YD596FLbKH6kc2ZHiaqbkPsTmWG9LHFWGcHfvSvyHiR3DvoKpejEcpo7/icEk93HHYeXbGSrafnmE+XM1dKsqH6pmmiIbt+n1o/6w0C6D7a3DF0lezPsIA6k96QSIETFYPZ00bS3hhsEBEopKVrI4GKLV9ALF7d9BNUSMlH9F08mYN7w07iq+kI9KLiwk2AxBARIkIMgogSkWhZuuPKT+e+g/LpC3GzkotKdny3lPyClpiXPyqGQY7tjY005SVSD78iyA1CmUlMmJIEOEgNrBzZP+QXZZrk3YUdkAzYd1phejIOitj9lgnMeO7xp2U9OaseiB4T6CpGL920SvXiMCZJ1vZZqomQBSyDK89Ld04/+YTl4aiC5+pSsXrmq9kRnt6PITl00w16NeUf/6X21zJHrEo/VEf946QDPBhuEFMLL+wPdM0gneB1dJGx8l7AWfM0wEyYA6Xo+VIhzv8kIFad63W+nZNPth3uplLs/fWbNdHPgTRM+ndRetal8Eeum7D7wYf9Rvt/+p+gltL5ATL2X0f3nbaUOVI+s6DxJhdHXF2iAHe9jFQ==&lt;/CipherValue&gt;&lt;/CipherData&gt;&lt;Signature xmlns=&quot;http://www.w3.org/2000/09/xmldsig#&quot;&gt;&lt;SignedInfo&gt;&lt;CanonicalizationMethod Algorithm=&quot;http://www.w3.org/TR/2001/REC-xml-c14n-20010315&quot; /&gt;&lt;SignatureMethod Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#rsa-sha1&quot; /&gt;&lt;Reference URI=&quot;&quot;&gt;&lt;Transforms&gt;&lt;Transform Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#enveloped-signature&quot; /&gt;&lt;/Transforms&gt;&lt;DigestMethod Algorithm=&quot;http://www.w3.org/2000/09/xmldsig#sha1&quot; /&gt;&lt;DigestValue&gt;v7au2Y4id9OukyDScJXdmyNJfcM=&lt;/DigestValue&gt;&lt;/Reference&gt;&lt;/SignedInfo&gt;&lt;SignatureValue&gt;ej0vFEJEU9k02lKCKxBSzVKOMQlnva8nG5p6RGqGzFa3GnhIvv6Mxie9hHRaoh0Wnxq/SmlTUQRvD3sELYKBTifLI2T8MnnEoZvyUaMUboEKbx/rYzH2PaT25QDWrRVGf3Xw5T9/xL3C7Bq5UxV5NiaRF6EwgmMU32VulZFbcti9kxWLMN/ZqmwE+HD9qv1bg2Zsd1pizPikRI+xSID4v7ZOOfiFowGwBMX4dBZabxR4j6Qkk8K5MFPxh9OnQ7SvLQQ61mNEGo+rI/PZ+y+75cWdLROIbzu7xE0npqJjmCgNmN6xcp2YYWYlTQiJda/w6BZ1ePkIJLtmBcv1+KSfcQ==&lt;/SignatureValue&gt;&lt;KeyInfo&gt;&lt;KeyName&gt;266CC02539F6BE76558C0FDEE89BAB5E0F0C12ED&lt;/KeyName&gt;&lt;/KeyInfo&gt;&lt;/Signature&gt;&lt;/EncryptedData&gt;" />
+    <!-- End inputs are required by the presentation framework. -->
+
+    <div class="groupMargin">
+      Enter your RSA SecurID passcode.
+    </div>
+
+    <div class="fieldMargin error smallText">
+      <label id="errorText" for=""></label>
+    </div>
+
+    <div>
+      <input id="passcodeInput" name="Passcode" type="password" value="" class="text fullWidth" placeholder="Passcode" tabindex="1" autocomplete="off" autofocus="" />
+    </div>
+
+    <div class="submitMargin">
+      <input id="submitButton" type="submit" name="Submit" tabindex="2" value="Submit" />
+      <img class="floatReverse" style="padding:6px 0px 6px 0px" width="90" height="18" alt="RSA logo" src="data:image/gif;base64,R0lGODlhWgASAOZ+AP7x8vBHVvFWY/7y8/zh4+whMvzW2v3s7e0lNv3r7P/9/e0vP/q/xPvV2OwiNP74+e0tPv719vvM0PaQmfFbaPza3fzX2u0qO/m4ve0oOfJhbv75+e9AT/WGkPFeavJkcP73+OwiM/WOl/R6hPWKk/ehqO0nOO4yQ+89TPm7wP/6+u45Sf3l5/3j5e4zQ/709PR4g+9HVf7z9P709e0rPPedpfvS1vipsPBTYfiss/inrvrCx/emrewfMfNxfPN0gP/7/PNtePBPXfBKWe9BUPSAiu0pO+9FVPR8h/3o6v3k5v/8/fvLz/aYoPR7he9GVfBIV/BRX/vP0/aRmv3m6P3p6+44SPJpdfWHke4xQvivtfm8wfBQXvekq/Nyfe8+TfBNW/JodPiwt/aTnPSDjfm5v+40RPN0f+0mN/R9iPSCjPFXZfzf4vNwe/FVYvR/ifrDyO4xQfm3vPNtef7t7/rFyfvT1u9AUPaaou0uP/m2vPFYZuweMAAAAP///wAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMTEgNzkuMTU4MzI1LCAyMDE1LzA5LzEwLTAxOjEwOjIwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjFmYWI4ZjEyLWYwOWEtNGE0OC1iZGI3LTFkYzEwY2VjZjYxOSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozQ0Y0MkVGQ0Y1MTcxMUU1ODAwQTgxOEM5QTg0RDQxOSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozQ0Y0MkVGQkY1MTcxMUU1ODAwQTgxOEM5QTg0RDQxOSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNSAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOmM1MDlmZmM3LTE0ZmQtNDhkMS1iMTE1LTc5ZWYzMmU2YmJhMSIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjZhYTZiZjc2LTNkODUtMTE3OS1hMTc4LWZhYzA5NTNhZDczZiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAEAAH4ALAAAAABaABIAAAf/gH6Cg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJUBm5wUiB4XfD0rYYkCAoM1mauDfK6vrnmEeK4ZYEdofBqHQ65ug3qsiH2ExIuuhQF8AYMmfI6wiX3TxpjVfteIyIXbft2KFHxRHLqH2ZnX54bfgt0nfByM2+yC59OD98bE+9jV1MX3sOFr9YwQET44CGVwdeIMoiB8UAgyw2eOuYD6BNbbmI/jxoEa+REU5OPVF0NqrLxaYKhAQT9N+BRQtO+fP4/9Omr8mPFaNwh89iia4uJbGj4QCNF4aW7nzp4feRaT6pMpHweMYvDh0oxpOwSFqPnDqE9nznoB0abjxlQInzXyUO60wloIAR8swiZlY+eSUAdDcfjAELSAjxdDTvhcyBvpHLsifLIMCuHKyIrCEUce6svY0ocPhjygGkQGit0QTwj9EDACUQcBVzrLnk27NqRAADs=" />
+    </div>
+
+  </form>
+</div>
+
+</div>      <div id="authOptions">
+        <form id="options"  method="post" action="https://id.example.com:443/adfs/ls/idpinitiatedsignon">
+            <script type="text/javascript">
+                function SelectOption(option) {
+                    var i = document.getElementById('optionSelection');
+                    i.value = option;
+                    document.forms['options'].submit();
+                    return false;
+                }
+            </script>
+            <input id="optionSelection" type="hidden" name="AuthMethod" />
+            <div class='groupMargin'></div>
+        </form>
+      </div>
+    </div>
+
+                </div>
+                <div id="footerPlaceholder"></div>
+            </div>
+            <div id="footer">
+                <div id="footerLinks" class="floatReverse">
+                     <div><span id="copyright">&#169; 2013 Microsoft</span></div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/pkg/provider/adfs2/ntlm.go b/pkg/provider/adfs2/ntlm.go
new file mode 100644
index 0000000..9d41f97
--- /dev/null
+++ b/pkg/provider/adfs2/ntlm.go
@@ -0,0 +1,44 @@
+package adfs2
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/pkg/errors"
+	"github.com/versent/saml2aws/pkg/creds"
+)
+
+func (ac *Client) authenticateNTLM(loginDetails *creds.LoginDetails) (string, error) {
+
+	ac.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+		req.SetBasicAuth(loginDetails.Username, loginDetails.Password)
+		return nil
+	}
+
+	url := fmt.Sprintf("%s/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=%s", loginDetails.URL, ac.idpAccount.AmazonWebservicesURN)
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return "", err
+	}
+	req.SetBasicAuth(loginDetails.Username, loginDetails.Password)
+
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return "", errors.Wrap(err, "error retieving login form")
+	}
+
+	data, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return "", errors.Wrap(err, "error retieving body")
+	}
+
+	doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(data))
+	if err != nil {
+		return "", errors.Wrap(err, "error parsing document")
+	}
+
+	return extractSamlAssertion(doc)
+}
diff --git a/pkg/provider/adfs2/rsa.go b/pkg/provider/adfs2/rsa.go
new file mode 100644
index 0000000..6d00eb1
--- /dev/null
+++ b/pkg/provider/adfs2/rsa.go
@@ -0,0 +1,198 @@
+package adfs2
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/pkg/errors"
+	prompt "github.com/segmentio/go-prompt"
+	"github.com/versent/saml2aws/pkg/creds"
+	"github.com/versent/saml2aws/pkg/dump"
+)
+
+// Authenticate authenticate the user using the supplied login details
+func (ac *Client) authenticateRsa(loginDetails *creds.LoginDetails) (string, error) {
+
+	authSubmitURL, authForm, err := ac.getLoginForm(loginDetails)
+	if err != nil {
+		return "", errors.Wrap(err, "error retrieving login form from idp")
+	}
+
+	doc, err := ac.postLoginForm(authSubmitURL, authForm)
+	if err != nil {
+		return "", errors.Wrap(err, "error posting login form to idp")
+	}
+
+	passcodeForm, passcodeActionURL, err := extractFormData(doc)
+	if err != nil {
+		return "", errors.Wrap(err, "error extracting mfa form data")
+	}
+
+	token := prompt.StringRequired("Enter passcode")
+
+	passcodeForm.Set("Passcode", token)
+	passcodeForm.Del("submit")
+
+	doc, err = ac.postPasscodeForm(passcodeActionURL, passcodeForm)
+	if err != nil {
+		return "", errors.Wrap(err, "error posting login form to idp")
+	}
+
+	rsaForm, rsaActionURL, err := extractFormData(doc)
+	if err != nil {
+		return "", errors.Wrap(err, "error extracting rsa form data")
+	}
+
+	nextCode := prompt.StringRequired("Enter nextCode")
+
+	rsaForm.Set("NextCode", nextCode)
+	rsaForm.Del("submit")
+
+	doc, err = ac.postRSAForm(rsaActionURL, rsaForm)
+	if err != nil {
+		return "", errors.Wrap(err, "error posting rsa form")
+	}
+
+	return extractSamlAssertion(doc)
+}
+
+func (ac *Client) postLoginForm(authSubmitURL string, authForm url.Values) (*goquery.Document, error) {
+
+	req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode()))
+	if err != nil {
+		return nil, errors.Wrap(err, "error building authentication request")
+	}
+
+	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+	logger.WithField("authSubmitURL", authSubmitURL).WithField("req", dump.RequestString(req)).Debug("POST")
+
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "error retrieving login form")
+	}
+
+	logger.WithField("status", res.StatusCode).WithField("authSubmitURL", authSubmitURL).WithField("res", dump.ResponseString(res)).Debug("POST")
+
+	doc, err := goquery.NewDocumentFromResponse(res)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to build document from response")
+	}
+
+	return doc, nil
+}
+
+func (ac *Client) getLoginForm(loginDetails *creds.LoginDetails) (string, url.Values, error) {
+
+	adfs2Url := fmt.Sprintf("%s/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=%s", loginDetails.URL, ac.idpAccount.AmazonWebservicesURN)
+
+	req, err := http.NewRequest("GET", adfs2Url, nil)
+	if err != nil {
+		return "", nil, err
+	}
+
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return "", nil, errors.Wrap(err, "error retrieving login form")
+	}
+
+	logger.WithField("status", res.StatusCode).WithField("url", loginDetails.URL).WithField("res", dump.ResponseString(res)).Debug("GET")
+
+	// REALLY need to extract the form and actionURL from the previous response
+
+	authForm := url.Values{}
+	authForm.Add("UserName", loginDetails.Username)
+	authForm.Add("Password", loginDetails.Password)
+	authForm.Add("AuthMethod", "FormsAuthentication")
+
+	authSubmitURL := fmt.Sprintf("%s/adfs/ls/idpinitiatedsignon", loginDetails.URL)
+
+	return authSubmitURL, authForm, nil
+}
+
+func (ac *Client) postPasscodeForm(passcodeActionURL string, passcodeForm url.Values) (*goquery.Document, error) {
+
+	req, err := http.NewRequest("POST", passcodeActionURL, strings.NewReader(passcodeForm.Encode()))
+	if err != nil {
+		return nil, errors.Wrap(err, "error building authentication request")
+	}
+
+	logger.WithField("actionURL", passcodeActionURL).WithField("req", dump.RequestString(req)).Debug("POST")
+
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "error retrieving login form")
+	}
+
+	logger.WithField("status", res.StatusCode).WithField("passcodeActionURL", passcodeActionURL).WithField("res", dump.ResponseString(res)).Debug("POST")
+
+	doc, err := goquery.NewDocumentFromResponse(res)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to build document from response")
+	}
+
+	return doc, nil
+}
+
+func (ac *Client) postRSAForm(rsaSubmitURL string, form url.Values) (*goquery.Document, error) {
+
+	req, err := http.NewRequest("POST", rsaSubmitURL, strings.NewReader(form.Encode()))
+	if err != nil {
+		return nil, errors.Wrap(err, "error building authentication request")
+	}
+
+	logger.WithField("rsaSubmitURL", rsaSubmitURL).WithField("req", dump.RequestString(req)).Debug("POST")
+
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "error retrieving login form")
+	}
+
+	logger.WithField("status", res.StatusCode).WithField("rsaSubmitURL", rsaSubmitURL).WithField("res", dump.ResponseString(res)).Debug("POST")
+
+	data, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return nil, errors.Wrap(err, "error retrieving body")
+	}
+
+	doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(data))
+	if err != nil {
+		return nil, errors.Wrap(err, "error parsing document")
+	}
+
+	return doc, nil
+}
+
+func extractFormData(doc *goquery.Document) (url.Values, string, error) {
+	formData := url.Values{}
+	var actionURL string
+
+	//get action url
+	doc.Find("form").Each(func(i int, s *goquery.Selection) {
+		action, ok := s.Attr("action")
+		if !ok {
+			return
+		}
+		actionURL = action
+	})
+
+	// extract form data to passthrough
+	doc.Find("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
+}
diff --git a/pkg/provider/keycloak/keycloak.go b/pkg/provider/keycloak/keycloak.go
index 63c4dbb..448793e 100644
--- a/pkg/provider/keycloak/keycloak.go
+++ b/pkg/provider/keycloak/keycloak.go
@@ -53,6 +53,9 @@ func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 	}
 
 	data, err := kc.postLoginForm(authSubmitURL, authForm)
+	if err != nil {
+		return "", fmt.Errorf("error submitting login form")
+	}
 	if authSubmitURL == "" {
 		return "", fmt.Errorf("error submitting login form")
 	}
diff --git a/saml2aws.go b/saml2aws.go
index bd6a722..6023c6e 100644
--- a/saml2aws.go
+++ b/saml2aws.go
@@ -20,8 +20,8 @@ type ProviderList map[string][]string
 // MFAsByProvider a list of providers with their respective supported MFAs
 var MFAsByProvider = ProviderList{
 	"ADFS":      []string{"Auto", "VIP"},
-	"ADFS2":     []string{"Auto"},
-	"Ping":      []string{"Auto"}, // automatically detects PingID
+	"ADFS2":     []string{"Auto", "RSA"}, // nothing automatic about ADFS 2.x
+	"Ping":      []string{"Auto"},        // automatically detects PingID
 	"JumpCloud": []string{"Auto"},
 	"Okta":      []string{"Auto"}, // automatically detects DUO, SMS and ToTP
 	"KeyCloak":  []string{"Auto"}, // automatically detects ToTP


Test patch:
diff --git a/pkg/provider/adfs2/rsa_test.go b/pkg/provider/adfs2/rsa_test.go
new file mode 100644
index 0000000..8adbf1b
--- /dev/null
+++ b/pkg/provider/adfs2/rsa_test.go
@@ -0,0 +1,86 @@
+package adfs2
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/PuerkitoBio/goquery"
+
+	"github.com/stretchr/testify/require"
+	"github.com/versent/saml2aws/pkg/cfg"
+	"github.com/versent/saml2aws/pkg/creds"
+)
+
+const (
+	exampleLoginURL = "https://id.example.com"
+)
+
+func TestClient_getLoginForm(t *testing.T) {
+
+	data, err := ioutil.ReadFile("example/loginpage.html")
+	require.Nil(t, err)
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Write(data)
+	}))
+	defer ts.Close()
+
+	c := Client{
+		idpAccount: &cfg.IDPAccount{AmazonWebservicesURN: ""},
+		client:     &http.Client{},
+	}
+	loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "test", Password: "test123"}
+
+	submitURL, authForm, err := c.getLoginForm(loginDetails)
+	require.Nil(t, err)
+	require.True(t, strings.HasSuffix(submitURL, "/adfs/ls/idpinitiatedsignon"))
+	require.Equal(t, url.Values{
+		"UserName":   []string{"test"},
+		"Password":   []string{"test123"},
+		"AuthMethod": []string{"FormsAuthentication"},
+	}, authForm)
+}
+
+func TestClient_postLoginForm(t *testing.T) {
+
+	data, err := ioutil.ReadFile("example/passcode.html")
+	require.Nil(t, err)
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Write(data)
+	}))
+	defer ts.Close()
+
+	loginForm := url.Values{
+		"UserName":   []string{"test"},
+		"Password":   []string{"test123"},
+		"AuthMethod": []string{"FormsAuthentication"},
+	}
+
+	c := Client{
+		idpAccount: &cfg.IDPAccount{AmazonWebservicesURN: ""},
+		client:     &http.Client{},
+	}
+	content, err := c.postLoginForm(ts.URL, loginForm)
+	require.Nil(t, err)
+	require.NotNil(t, content)
+}
+
+func TestClient_extractFormData(t *testing.T) {
+
+	file, err := os.Open("example/passcode.html")
+	require.Nil(t, err)
+
+	doc, err := goquery.NewDocumentFromReader(file)
+	require.Nil(t, err)
+	form, actionURL, err := extractFormData(doc)
+	require.Nil(t, err)
+	require.Equal(t, "https://id.example.com:443/adfs/ls/idpinitiatedsignon", actionURL)
+	require.Equal(t, "", form.Get("Passcode"))
+	require.Equal(t, "Submit", form.Get("Submit"))
+}