Repository: Versent/saml2aws
Pull request #288 (2019-03-03 20:58:38)

feature: Add f5apm provider
Support for F5's Access Policy Manager as an additional identity provider. 
* Automatic detection of MFA
* Automatic detection of MFA options (push, token)

six-arm: Here's an example of a passing build for the branch.  https://travis-ci.com/six-arm/saml2aws/builds/103085564.  Please let me know what else is needed for considering this PR.

Patch:
diff --git a/README.md b/README.md
index e8dae5d..8529ade 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ The process goes something like this:
   * KeyCloak + (TOTP)
   * [Google Apps](pkg/provider/googleapps/README.md)
   * [Shibboleth](pkg/provider/shibboleth/README.md)
+  * [F5APM](pkg/provider/f5apm/README.md)
   * [PSU](pkg/provider/psu/README.md)
 * AWS SAML Provider configured
 
diff --git a/cmd/saml2aws/main.go b/cmd/saml2aws/main.go
index b5dfbf3..01b5c88 100644
--- a/cmd/saml2aws/main.go
+++ b/cmd/saml2aws/main.go
@@ -1,10 +1,10 @@
 package main
 
 import (
+	"crypto/tls"
 	"fmt"
-	"os"
 	"net/http"
-	"crypto/tls"
+	"os"
 
 	"github.com/alecthomas/kingpin"
 	"github.com/sirupsen/logrus"
@@ -53,7 +53,7 @@ func main() {
 	// Common (to all commands) settings
 	commonFlags := new(flags.CommonFlags)
 	app.Flag("idp-account", "The name of the configured IDP account. (env: SAML2AWS_IDP_ACCOUNT)").Envar("SAML2AWS_IDP_ACCOUNT").Short('a').Default("default").StringVar(&commonFlags.IdpAccount)
-	app.Flag("idp-provider", "The configured IDP provider. (env: SAML2AWS_IDP_PROVIDER)").Envar("SAML2AWS_IDP_PROVIDER").EnumVar(&commonFlags.IdpProvider, "ADFS", "ADFS2", "Ping", "JumpCloud", "Okta", "OneLogin", "PSU", "KeyCloak")
+	app.Flag("idp-provider", "The configured IDP provider. (env: SAML2AWS_IDP_PROVIDER)").Envar("SAML2AWS_IDP_PROVIDER").EnumVar(&commonFlags.IdpProvider, "ADFS", "ADFS2", "Ping", "JumpCloud", "Okta", "OneLogin", "PSU", "KeyCloak", "F5APM")
 	app.Flag("mfa", "The name of the mfa. (env: SAML2AWS_MFA)").Envar("SAML2AWS_MFA").StringVar(&commonFlags.MFA)
 	app.Flag("skip-verify", "Skip verification of server certificate. (env: SAML2AWS_SKIP_VERIFY)").Envar("SAML2AWS_SKIP_VERIFY").Short('s').BoolVar(&commonFlags.SkipVerify)
 	app.Flag("url", "The URL of the SAML IDP server used to login. (env: SAML2AWS_URL)").Envar("SAML2AWS_URL").StringVar(&commonFlags.URL)
@@ -72,6 +72,7 @@ func main() {
 	cmdConfigure.Flag("client-secret", "OneLogin client secret, used to generate API access token. (env: ONELOGIN_CLIENT_SECRET)").Envar("ONELOGIN_CLIENT_SECRET").StringVar(&commonFlags.ClientSecret)
 	cmdConfigure.Flag("subdomain", "OneLogin subdomain of your company account. (env: ONELOGIN_SUBDOMAIN)").Envar("ONELOGIN_SUBDOMAIN").StringVar(&commonFlags.Subdomain)
 	cmdConfigure.Flag("profile", "The AWS profile to save the temporary credentials. (env: SAML2AWS_PROFILE)").Envar("SAML2AWS_PROFILE").Short('p').StringVar(&commonFlags.Profile)
+	cmdConfigure.Flag("resource-id", "F5APM SAML resource ID of your company account. (env: SAML2AWS_F5APM_RESOURCE_ID)").Envar("SAML2AWS_F5APM_RESOURCE_ID").StringVar(&commonFlags.ResourceID)
 	configFlags := commonFlags
 
 	// `login` command and settings
@@ -121,7 +122,7 @@ func main() {
 	}
 
 	// Set the default transport settings so all http clients will pick them up.
-	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: commonFlags.SkipVerify,}
+	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: commonFlags.SkipVerify}
 	http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment
 
 	logrus.WithField("command", command).Debug("Running")
diff --git a/input.go b/input.go
index 927dff8..f4c180a 100644
--- a/input.go
+++ b/input.go
@@ -37,18 +37,17 @@ func PromptForConfigurationDetails(idpAccount *cfg.IDPAccount) error {
 
 	idpAccount.Profile = prompter.String("AWS Profile", idpAccount.Profile)
 
-	fmt.Println("")
-
 	idpAccount.URL = prompter.String("URL", idpAccount.URL)
 	idpAccount.Username = prompter.String("Username", idpAccount.Username)
 
-	fmt.Println("")
-
-	if idpAccount.Provider == "OneLogin" {
+	switch idpAccount.Provider {
+	case "OneLogin":
 		idpAccount.AppID = prompter.String("App ID", idpAccount.AppID)
 		fmt.Println("")
 		idpAccount.Subdomain = prompter.String("Subdomain", idpAccount.Subdomain)
 		fmt.Println("")
+	case "F5APM":
+		idpAccount.ResourceID = prompter.String("Resource ID", idpAccount.ResourceID)
 	}
 
 	return nil
diff --git a/pkg/cfg/cfg.go b/pkg/cfg/cfg.go
index 904b57b..15cc9ad 100644
--- a/pkg/cfg/cfg.go
+++ b/pkg/cfg/cfg.go
@@ -40,19 +40,24 @@ type IDPAccount struct {
 	AmazonWebservicesURN string `ini:"aws_urn"`
 	SessionDuration      int    `ini:"aws_session_duration"`
 	Profile              string `ini:"aws_profile"`
-	Subdomain            string `ini:"subdomain"` // used by OneLogin
+	ResourceID           string `ini:"resource_id"` // used by F5APM
+	Subdomain            string `ini:"subdomain"`   // used by OneLogin
 	RoleARN              string `ini:"role_arn"`
 }
 
 func (ia IDPAccount) String() string {
 	var appID string
-	if ia.Provider == "OneLogin" {
+	var policyID string
+	switch ia.Provider {
+	case "OneLogin":
 		appID = fmt.Sprintf(`
   AppID: %s
   Subdomain: %s`, ia.AppID, ia.Subdomain)
+	case "F5APM":
+		policyID = fmt.Sprintf("\n  ResourceID: %s", ia.ResourceID)
 	}
 
-	return fmt.Sprintf(`account {%s
+	return fmt.Sprintf(`account {%s%s
   URL: %s
   Username: %s
   Provider: %s
@@ -62,18 +67,23 @@ func (ia IDPAccount) String() string {
   SessionDuration: %d
   Profile: %s
   RoleARN: %s
-}`, appID, ia.URL, ia.Username, ia.Provider, ia.MFA, ia.SkipVerify, ia.AmazonWebservicesURN, ia.SessionDuration, ia.Profile, ia.RoleARN)
+}`, appID, policyID, ia.URL, ia.Username, ia.Provider, ia.MFA, ia.SkipVerify, ia.AmazonWebservicesURN, ia.SessionDuration, ia.Profile, ia.RoleARN)
 }
 
 // Validate validate the required / expected fields are set
 func (ia *IDPAccount) Validate() error {
-	if ia.Provider == "OneLogin" {
+	switch ia.Provider {
+	case "OneLogin":
 		if ia.AppID == "" {
 			return errors.New("app ID empty in idp account")
 		}
 		if ia.Subdomain == "" {
 			return errors.New("subdomain empty in idp account")
 		}
+	case "F5APM":
+		if ia.ResourceID == "" {
+			return errors.New("Resource ID empty in idp account")
+		}
 	}
 
 	if ia.URL == "" {
@@ -166,7 +176,7 @@ func (cm *ConfigManager) LoadIDPAccount(idpAccountName string) (*IDPAccount, err
 		return nil, errors.Wrap(err, "Unable to load configuration file")
 	}
 
-	// attempt to map a specific idp account by name	
+	// attempt to map a specific idp account by name
 	// this will return an empty account if one is not found by the given name
 	account, err := readAccount(idpAccountName, cfg)
 	if err != nil {
diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go
index 5a78943..483b6a4 100644
--- a/pkg/flags/flags.go
+++ b/pkg/flags/flags.go
@@ -23,6 +23,7 @@ type CommonFlags struct {
 	SkipVerify           bool
 	Profile              string
 	Subdomain            string
+	ResourceID           string
 }
 
 // LoginExecFlags flags for the Login / Exec commands
@@ -77,4 +78,7 @@ func ApplyFlagOverrides(commonFlags *CommonFlags, account *cfg.IDPAccount) {
 	if commonFlags.RoleArn != "" {
 		account.RoleARN = commonFlags.RoleArn
 	}
+	if commonFlags.ResourceID != "" {
+		account.ResourceID = commonFlags.ResourceID
+	}
 }
diff --git a/pkg/provider/f5apm/README.md b/pkg/provider/f5apm/README.md
new file mode 100644
index 0000000..62802a5
--- /dev/null
+++ b/pkg/provider/f5apm/README.md
@@ -0,0 +1,40 @@
+# F5 Access Policy Manager Provider
+
+* https://www.f5.com/products/security/access-policy-manager
+
+## Instructions
+
+You'll need the SAML policy ID for the AWS account.  Your admin should be able to 
+provide this (or you'll briefly see it in a redirect when you click an application link)
+
+```
+https://<YOUR ORGS DOMAIN>/saml/idp/res?id=<SAML RESOURCE ID>
+```
+
+Example Config:
+
+```
+[default]
+url                  = https://<YOUR ORGS DOMAIN>
+username             = <YOUR USERNAME>
+provider             = F5APM
+mfa                  = Auto
+skip_verify          = false
+timeout              = 0
+aws_urn              = urn:amazon:webservices
+aws_session_duration = 3600
+aws_profile          = <AWS PROFILE NAME>
+resource_id          = <SAML RESOURCE ID>
+role_arn             = 
+```
+
+Where `resource_id` will be something like `/Common/example-aws-account`
+
+## Features
+
+* Automatic detection of MFA
+* Automatic detection of MFA options (push, token)
+
+## More Details
+
+* https://devcentral.f5.com/articles/configuration-example-big-ip-apm-as-saml-idp-for-amazon-web-services
\ No newline at end of file
diff --git a/pkg/provider/f5apm/example/loginpage.html b/pkg/provider/f5apm/example/loginpage.html
new file mode 100644
index 0000000..8392169
--- /dev/null
+++ b/pkg/provider/f5apm/example/loginpage.html
@@ -0,0 +1,782 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta http-equiv="pragma" content="no-cache">
+<meta http-equiv="cache-control" content="no-cache">
+<title>federate.example.com</title>
+<link rel="stylesheet" type="text/css" HREF="/public/include/css/apm.css">
+<script language="JavaScript" src="/public/include/js/session_check.js?v=13" ></script>
+<script language="JavaScript" src="/public/include/js/agent_common.js" ></script>
+<script language="JavaScript" src="/public/include/js/web_host.js" ></script>
+<script language="javascript">
+<!--
+if(!String.prototype.trim){ String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); }; }
+
+/////////////////////////////////////////////////////////////////////////
+// function to return the customized screen when session expiration
+// is detected on the client-side
+/////////////////////////////////////////////////////////////////////////
+function SessionExpired_CustomizedScreenGet()
+{
+  return '<br><h3>Session Expired/Timeout</h3><br>Due to user inactivity, your session expired. Click the following link to start a new session.<br><br><table id="IHoptions"><tr><td><a href="[SESSION_RESTART_URL]" class="option"><img src="/public/images/sq-positive.gif" ></a><a href="[SESSION_RESTART_URL]">Start a new session</a></td></tr></table>';
+}
+
+var globalRestartOnSubmit = false;
+
+function sessionTimedOut()
+{
+    // restart session automatically upon "submit" for edge-like clients if logon form is compatibile
+    if (externalWebHost.hasWebLogonResetSession() && logonFormCompatible) {
+       globalRestartOnSubmit = true;
+    }
+    else {
+       // display session expired msg for non-compatible forms or
+       // non-edge clients
+       sessionTimeout.showSplashLayer("MessageDIV", SessionExpired_CustomizedScreenGet());
+    }
+}
+
+/**
+ * Place the focus and init the form fields
+ */
+
+if(self != top) { top.location = self.location; }
+window.onerror=function(){ return function(){ return; } }
+
+//--------------------------------------------------------------------
+var doAutoSubmit = true;
+
+var globalFormId = 'auth_form';
+var globalTableId = 'credentials_table';
+var globalSubmitTrId = 'submit_row';
+
+var globalSavePasswordCheckbox = null;
+var logonFormCompatible = false;
+
+var softTokenInput = null;
+var softTokenFieldId = "";
+var softTokenState = "";
+var softTokenNewPIN = "";
+var softTokenPINAutoPopulate = false;
+if( !softTokenState ) {
+    softTokenState = "SECURID_AUTH_STATE_START";}
+
+    var challengeMode = false;
+
+function getInputField(fieldId)
+{
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return null;
+    }
+
+    if(fieldId == null || fieldId == ""){
+        return null;
+    }
+
+    var inputs = form.getElementsByTagName("input");
+    for( var i=0; i<inputs.length; i++ ){
+        if(  inputs[i].name == fieldId){
+            return inputs[i];
+        }
+    }
+    return null;
+}
+
+function getSoftTokenInput()
+{
+    return getInputField(softTokenFieldId);
+}
+
+function getUsernameInput()
+{
+    return getInputField("username");
+}
+
+function getPasswordInput()
+{
+    return getInputField("password");
+}
+
+function edgeClientSoftTokenSupport()
+{
+    try {
+        return externalWebHost.hasWebLogonSoftTokenSupport();
+    } catch(e) {}
+    return false;
+}
+
+function getSoftTokenPrompt()
+{
+    if ( softTokenFieldId != "" && edgeClientSoftTokenSupport()) {
+        var div = document.getElementById("formHeaderSoftToken");
+        if (div)  {
+            return div.innerHTML;
+        }
+    }
+    return  null;
+}
+
+function setSoftTokenChallengeResponse(challengeResponse)
+{
+    var challengeElements = document.getElementsByName('_F5_challenge');
+    if((challengeElements.length == 0) || (null == challengeElements[0])) {
+        return;
+    }
+
+    var challengeElement = challengeElements[0];
+    if(challengeElement.tagName.toLowerCase() == 'select') {
+        var challengeOptions = challengeElement.options;
+        if(null == challengeOptions){
+            return;
+        }
+        for (var i = 0, optionsLength = challengeOptions.length; i < optionsLength; i++) {
+            if (challengeOptions[i].value == challengeResponse) {
+                challengeElement.selectedIndex = i;
+                break;
+            }
+        }
+    }else if(challengeElement.tagName.toLowerCase() == 'input') {
+        challengeElement.value = challengeResponse;
+    }
+}
+
+function OnSubmitEdgeRSASoftToken()
+{
+    var support = edgeClientSoftTokenSupport();
+    if( true != support ) {
+        return;
+    }
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+    var inputs = form.getElementsByTagName("input");
+
+    var hiddenInput = document.createElement("input");
+    hiddenInput.setAttribute("type", "hidden");
+        hiddenInput.setAttribute("name", "softTokenError");
+    //append to form element that you want .
+    form.appendChild(hiddenInput);
+
+    try{
+        if(softTokenInput != null) { //normal mode
+            // if the client cannot request soft-token PIN,
+            // ask user to enter it in the logon page and pass the value to the client
+            // Otherwise, the client already has the PIN
+            if(!softTokenPINAutoPopulate) {
+                externalWebHost.setWebLogonSoftTokenPIN(softTokenInput.value);
+            }
+            var passcode = externalWebHost.getWebLogonSoftTokenPasscode();
+            if( passcode ) {
+                softTokenInput.value = passcode;
+            }
+        } else if( challengeMode ) { //challenge mode
+            // If the client can provide response to the challenge, use it instead of user input
+            if(externalWebHost.hasWeblogonSoftTokenChallengeResponse()){
+                var challengeResponse = externalWebHost.getWeblogonSoftTokenChallengeResponse();
+                if(null != challengeResponse){
+                     setSoftTokenChallengeResponse(challengeResponse);
+                }
+            }else{
+                // Request user input by default
+                externalWebHost.setWebLogonSoftTokenPIN(inputs[0].value);
+
+                var passcode = externalWebHost.getWebLogonSoftTokenPasscode();
+                if( passcode ) {
+                    inputs[0].value = passcode;
+                }
+            }
+
+        }
+        hiddenInput.value = externalWebHost.getWebLogonSoftTokenError();
+    } catch(e) { }
+
+    return;
+}
+
+// check whether logon form is compatible with the client for auto-population and auto-submission
+// only "username", "password" and soft token field (the name is stored in softTokenFieldId ) are supported
+// fields can be configured in either order, but can only be text, password or checkbox types
+// form is not considered compatible if it contains any additional fields not supported by the client
+// The form is compatible even if it contains a subset of the supported fields
+// In this case the client will auto-populate only those fields
+function getFormCompatibility()
+{
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return false;
+    }
+    // check if form suites
+    var inputs = [];
+    var inputsTemp = form.getElementsByTagName("input");
+    // filter submit, reset, hidden and little green men
+    for( var i=0; i<inputsTemp.length; i++ ){
+        if( inputsTemp[i].type == "text" || inputsTemp[i].type == "password"){
+            inputs[ inputs.length ] = inputsTemp[i];
+        }
+    }
+
+    var softTokenSupported = edgeClientSoftTokenSupport() && (null != softTokenInput);
+    // Check if there are any custom fields that are not supported by Edge Client for auto-population and auto-submission
+    for( var i=0; i<inputs.length; i++ ){
+       if((inputs[i].type == "text" && inputs[i].name == "username") ||
+          (inputs[i].type == "password" && inputs[i].name == "password") ||
+          (softTokenSupported && inputs[i].type == "password" && inputs[i].name == softTokenInput.name)){
+               continue;
+       }else {
+          return false;
+       }
+    }
+    return true;
+}
+
+function setOrigUriLink()
+{
+    var params = parseQueryParams(window.location.search.substr(1));
+    if (!params.hasOwnProperty('ORIG_URI')) {
+        return;
+    }
+
+    var credTable = document.getElementById('credentials_table');
+    if (credTable == null) {
+        return;
+    }
+    var tBody = credTable.tBodies[0];
+    if (tBody == null) {
+        return;
+    }
+
+    var trTag = document.createElement("TR");
+    tBody.insertBefore(trTag, tBody.children[tBody.children.length - 1]);
+
+    var tdTag = document.createElement("TD");
+    tdTag.setAttribute("class", "credentials_table_unified_cell");
+    tdTag.setAttribute("colspan", "2");
+    trTag.appendChild(tdTag);
+
+    var tdText = document.createTextNode("Click here if already logged in ");
+    tdTag.appendChild(tdText);
+
+    var origUri = atob(decodeURIComponent(params['ORIG_URI']));
+    var tdLink = document.createElement("A");
+    tdLink.innerHTML = origUri;
+    tdLink.setAttribute("href", origUri);
+    tdTag.appendChild(tdLink);
+
+}
+
+function OnLoad()
+{
+    var header = document.getElementById("credentials_table_header");
+    var softTokenHeaderStr = getSoftTokenPrompt();
+    if ( softTokenHeaderStr ) {
+        header.innerHTML = softTokenHeaderStr;
+    }
+    setFormAttributeByQueryParams("auth_form", "action", "/subsession_logon_submit.php3");
+    setFormAttributeByQueryParams("v2_original_url", "href", "/subsession_logon_submit.php3");
+    setOrigUriLink();
+
+    // check if form suites
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+    // check if form suites
+    var inputs = form.getElementsByTagName("input");
+    // filter submit, reset, hidden and little green men
+    for( var i=0; i<inputs.length; i++ ){
+        if( ( inputs[i].type == "text" || inputs[i].type == "password" ) && inputs[i].value == "" ){
+            inputs[i].focus();
+            if (inputs[i].type == "password") {
+                window.setTimeout( function(elem){ return function(){ elem.blur(); elem.focus(); } }(inputs[i]) , 266 );
+            }
+            return;
+        }
+    }
+}
+
+function disableSubmit(form)
+{
+    // disable!
+    var inputs = form.getElementsByTagName( "input" );
+    for( var i=0; i<inputs.length; i++ ){
+        if( inputs[i].type == "submit" || inputs[i].type == "reset" ){
+            inputs[i].disabled = true;
+        }
+    }
+
+    return true;
+}
+
+//This function is called from Edge Client. Update setWeblogonCallbacks call if renamed
+function challengeAutoSubmit()
+{
+    if(!challengeMode) {
+       return false;
+    }
+
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+       return false;
+    }
+
+    disableSubmit(form);
+
+    form.onsubmit();
+    form.submit();
+    return true;
+}
+
+//This function is called from Edge Client. Update setWeblogonCallbacks call if renamed
+function weblogonAutoSubmit()
+{
+      if(!logonFormCompatible){
+          return false;
+      }
+
+      var form = document.getElementById( globalFormId );
+      if( form == null ){
+          return false;
+      }
+
+      // autosubmit check
+      if(externalWebHost.hasWebLogonAutoLogon() && externalWebHost.getWebLogonAutoLogon() && doAutoSubmit){
+
+         disableSubmit(form);
+
+         form.onsubmit();
+         form.submit();
+         return true;
+      }else{
+        return false;
+      }
+}
+
+
+// support for autologon from Client API
+function checkExternalAddCheckbox()
+{
+    var checkbox_txt = 'Save Password';
+
+    if( !logonFormCompatible ){
+        try {
+            if (externalWebHost.hasWebLogonNotifyUser()){
+                externalWebHost.WebLogonNotifyUser();
+            }
+        } catch(e){};
+        return;
+    }
+
+    // find form
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+
+    // find table
+    var table = document.getElementById( globalTableId );
+    if( table == null ){
+        return;
+    }
+
+    // find tr
+    var submitTr = document.getElementById( globalSubmitTrId );
+    if( submitTr == null ){
+        return;
+    }
+
+    try {
+        if(externalWebHost.isAvailable()){
+            // push values
+            var usernameInput = getUsernameInput();
+            if( null != usernameInput && externalWebHost.hasWebLogonUserName() && usernameInput.value == ""){
+                usernameInput.value = externalWebHost.getWebLogonUserName();
+            }
+
+            var passwordInput = getPasswordInput();
+                        var allowSavingPassword = ((null != passwordInput) && (passwordInput.name != softTokenFieldId));
+            //don't populate RSA SecurID token field with the cached password
+            if(allowSavingPassword) {
+                if( externalWebHost.hasWebLogonPassword() && (passwordInput.value == "")){
+                     passwordInput.value =  externalWebHost.getWebLogonPassword();
+                }
+            }else{
+                doAutoSubmit = false;
+            }
+
+            // push data to cells
+            if(allowSavingPassword && externalWebHost.isWebLogonSavePasswordAvailable()){
+                // right - text
+
+                // create cells
+                var newTr = table.insertRow( submitTr.rowIndex );
+                var leftTd = newTr.insertCell( 0 );
+                var rightTd = newTr.insertCell( 1 );
+
+                leftTd.className = "credentials_table_label_cell";
+                rightTd.className = "credentials_table_field_cell credentials_table_field_checkbox_fix";
+
+                rightTd.innerHTML = checkbox_txt;
+                // left - checkbox
+                globalSavePasswordCheckbox = document.createElement("input");
+                globalSavePasswordCheckbox.type = "checkbox";
+                globalSavePasswordCheckbox.className = "credentials_input_checkbox";
+                globalSavePasswordCheckbox.value = 1;
+                globalSavePasswordCheckbox = leftTd.appendChild( globalSavePasswordCheckbox );
+                globalSavePasswordCheckbox.checked = externalWebHost.getWebLogonSavePasswordChecked();
+                if( globalSavePasswordCheckbox.autocomplete ) {
+                    globalSavePasswordCheckbox.autocomplete = "off";
+                }
+            }
+
+            // autosubmit if possible
+            if(weblogonAutoSubmit()){
+               return;
+            }
+        }
+    } catch (e) { }
+}
+
+function OnSubmit()
+{
+    // find form
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+    try{
+      if( externalWebHost.isAvailable() ){
+         // pass weblogon credentials back to the client
+         if( logonFormCompatible ){
+              var usernameInput = getUsernameInput();
+              if( null != usernameInput && externalWebHost.hasWebLogonUserName() ){
+                  externalWebHost.setWebLogonUserName(usernameInput.value);
+              }
+
+              var passwordInput = getPasswordInput();
+              if( null != passwordInput && externalWebHost.hasWebLogonPassword() ){
+                  externalWebHost.setWebLogonPassword(passwordInput.value);
+              }
+              // pass user decision to save the password back to the client
+              if( externalWebHost.hasWebLogonSavePasswordChecked() ){
+                  externalWebHost.setWebLogonSavePasswordChecked((null != globalSavePasswordCheckbox) && globalSavePasswordCheckbox.checked);
+              }
+          }
+          if (softTokenFieldId != "") {
+              OnSubmitEdgeRSASoftToken();
+          }
+      }
+    } catch(e) { }
+
+    return;
+}
+
+function verifyNewPassword()
+{
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return true;
+    }
+
+    var inputs = form.getElementsByTagName("input");
+    if( inputs.length >= 2 && inputs[0].name == "_F5_challenge" && inputs[0].type == "password" && inputs[1].name == "_F5_verify_password" && inputs[1].type == "password" ){
+        if( inputs[0].value != inputs[1].value ){
+            alert("Password and confirmation do not match.");
+            inputs[0].focus();
+            return false;
+        } else {
+            // Not sending the second field.
+            inputs[1].disabled = true;
+
+            try{
+                  if( externalWebHost.hasWebLogonPassword() ){
+                      externalWebHost.setWebLogonPassword(inputs[0].value);
+                  }
+            } catch(e) { }
+        }
+    }
+    return true;
+}
+
+function masterSubmit(form)
+{
+    if( !verifyNewPassword() /* || ... */){
+        return false;
+    }
+
+    OnSubmit(); // this required by edge
+
+    if (globalRestartOnSubmit) {
+        try {
+            var usernameInput = getUsernameInput();
+            var passwordInput = getPasswordInput();
+            externalWebHost.setWebLogonAutoLogon((usernameInput != null ) && ("" != usernameInput.value) && (null != passwordInput) && ("" != passwordInput.value));
+        }
+        catch (e) {}
+        // restart session
+        externalWebHost.webLogonResetSession();
+        return false;
+    }
+
+    disableSubmit(form);
+    return true;
+}
+//-->
+</script>
+</head>
+
+<body onload="OnLoad()">
+
+
+<!-- CSS Order Is Important, order should be Shared-UI, Shared-Logos, Custom-Skin -->
+<link id="customcss" rel="stylesheet" type="text/css" href="https://cdn.example.com/websites/globalapm/css/shared-logos-text.css"> 
+<link id="customcss" rel="stylesheet" type="text/css" href="https://cdn.example.com/websites/globalapm/css/shared-ui.css"> 
+<link id="customcss" rel="stylesheet" type="text/css" href="https://cdn.example.com/websites/globalapm/css/federate-skin.css">
+
+<meta name="viewport" id="viewport" content="initial-scale=1.0" />       
+<meta name="robots" context="noindex,nofollow">
+<script language="javascript">
+//orientation toolbox
+function setViewport(){
+    document.getElementById("viewport").setAttribute('content', Math.abs(window.orientation) == 90 ? 'width=device-height, initial-scale=1' : 'width=device-width, initial-scale=1.0' );
+}
+setViewport();
+window.onorientationchange = function() {
+    setViewport();
+}    
+</script>
+<table id="page_header">
+  <tr >
+    <td id="header_leftcell"><img src='/public/images/my/flogo.png'/></td>             
+    <td id="new_header_leftcell"><img src='https://cdn.example.com/websites/globalapm/logos/logo-federate.svg'/></td>             
+    <td id="header_rightcell"></td>
+  </tr>
+  <tr>
+     <td></td>
+     <td id="maxTimeoutTd"><span id="maxTimeoutTag" title=""></span></td>
+  </tr>
+</table>
+<noscript>
+<div id="noscript_warning_red">JavaScript is not enabled. Please enable JavaScript in your browser or contact your system administrator for assistance.</div>
+<div id="noscript_warning_newsession">To open a new session, please  <A href="/">click here.</A></div>
+</noscript>
+
+<table id="main_table" class="logon_page">
+<tr>
+        <td id="main_table_info_cell">
+    <form id="auth_form" name="e1" method="post" onsubmit="javascript: return masterSubmit(this);" autocomplete="off">
+    <table id="credentials_table">
+    <tr>
+        <td colspan=2 id="credentials_table_header" ><span class="info-text">Please sign in below</span></td>
+    </tr>
+    <tr>
+        <td colspan=2 id="credentials_table_postheader" ></td>
+    </tr>
+    <tr>
+        <td colspan=2 class="credentials_table_unified_cell" ><label for='input_1' id='label_input_1'>Username</label><input type='text' name='username' class='credentials_input_text' value='' id='input_1' autocomplete='off' autocapitalize='off' /></td>
+    </tr>
+    <tr>
+        <td colspan=2 class="credentials_table_unified_cell" ><label for='input_2' id='label_input_2'>Password</label><input type='password' name='password' class='credentials_input_password' value='' id='input_2' autocomplete='off' autocapitalize='off' /></td>
+    </tr>
+    <tr id="submit_row">
+        <td class="credentials_table_unified_cell"><input type=submit class="credentials_input_submit" value="Sign In"></td>
+    </tr>
+    <tr>
+        <td colspan=2 id="credentials_table_footer" ></td>
+    </tr>
+    </table>
+    <input type=hidden name="vhost" value="standard">
+    </form>
+    <script language="javascript"><!--//
+        softTokenInput = getSoftTokenInput();
+        logonFormCompatible = getFormCompatibility();
+        // Pass method names to the client for auto-submit triggered by the client
+        externalWebHost.setWeblogonCallbacks(
+             "weblogonAutoSubmit();",
+             "challengeAutoSubmit();"
+        );
+
+        // Check if the softTokenInput exists or in case of RSA challenge mode
+        if((softTokenFieldId != "") && (null != softTokenInput || challengeMode ) && edgeClientSoftTokenSupport()){
+            externalWebHost.setWebLogonSoftTokenPrompt(getSoftTokenPrompt());
+            externalWebHost.setWebLogonSoftTokenState(softTokenState);
+        }
+
+        // Check if the client can ask user for soft-token PIN
+        if((softTokenFieldId != "") && (softTokenInput != null)
+               && edgeClientSoftTokenSupport()
+               && externalWebHost.canRequestWeblogonSoftTokenPIN()){
+            softTokenInput.readOnly = true;
+            softTokenPINAutoPopulate = true;
+            // No need to request input of RSA PIN, as the client can show PIN input UI if needed,
+            // make the field read-only
+            // set bogus value to display
+            softTokenInput.value = "********";
+        }
+
+        checkExternalAddCheckbox();
+        setTimeout(function(){ window.sessionTimeout = new APMSessionTimeout(sessionTimedOut); }, 200);
+
+        function dontChangePasswordClick(){
+            var checkbox = document.getElementById("dont_change_password_checkbox");
+            var password = document.getElementById("input_1");
+            var verify = document.getElementById("input_2");
+            if( checkbox.checked ){
+                password.value = "";
+                password.disabled = true;
+                verify.value = "";
+                verify.disabled = true;
+            }else{
+                password.disabled = false;
+                verify.disabled = false;
+            }
+        }
+
+        var finitvalues=['','','','',''];
+        var sessionLogonCustomizations = ["","","","",""];
+        var sessionLogonCustomizationPairs = [[], [], [], [], []];
+        for( var i=0; i<sessionLogonCustomizations.length; i++ ){
+            var pairs = ( sessionLogonCustomizations[i].indexOf(";") == -1 ? [ sessionLogonCustomizations[i] ] : sessionLogonCustomizations[i].trim().split(";") );
+            for( var j=0; j<pairs.length; j++ ){
+                if( pairs[j].indexOf("=>") != -1 ){
+                    var pair = pairs[j].split("=>");
+                    sessionLogonCustomizationPairs[i][pair[0]] = pair[1];
+                }
+            }
+        }
+
+        var sessionLogonValuesets = [ "", "", "", "", "" ];
+        for( var i=0; i<sessionLogonValuesets.length; i++ ){
+            var inpx = document.getElementById("input_" + (i+1) );
+            if( inpx !== null && inpx.tagName.toUpperCase() == "SELECT" ){
+                if( sessionLogonValuesets[i].trim() != "" ){
+                    inpx.options.length = 0;
+                    var options = sessionLogonValuesets[i].trim().split(";");
+                    for( var j=0; j<options.length; j++ ){
+                        inpx.options.add( new Option( options[j], options[j] ) );
+                    }
+                }
+                if( sessionLogonCustomizations[i].trim() != "" ){
+                    for( var j=0; j<inpx.options.length; j++ ){
+                        if( typeof sessionLogonCustomizationPairs[i][ inpx.options[j].value ] != "undefined" ){
+                            inpx.options[j].text = sessionLogonCustomizationPairs[i][ inpx.options[j].value ];
+                        }
+                    }
+                }
+                // set value
+                for( var j=0; j<inpx.options.length; j++ ){
+                    if( inpx.options[j].value == finitvalues[i] ){
+                        inpx.value = finitvalues[i];
+                        break;
+                    }
+                }
+            }else if( document.getElementById("input_" + (i+1) + "_0" ) ){ // RADIO
+                var initValue = finitvalues[i];
+                var radio = null;
+                var name = document.getElementById("input_" + (i+1) + "_0" ).name;
+                if( sessionLogonValuesets[i].trim() != "" ){
+                    var parent = document.getElementById( "label_input_" + (i+1) ).parentNode;
+                    while( parent.childNodes.length > 1 ){
+                        parent.removeChild( parent.lastChild );
+                    }
+                    var options = sessionLogonValuesets[i].trim().split(";");
+                    for( var j=0; j<options.length; j++ ){
+                        var div = parent.appendChild( document.createElement( "div" ) );
+                        var elmdef = document.all && navigator.userAgent.match(/MSIE (\d+)/)[1] < 9 ? "<input type='radio'>" : "input";
+                        var input = div.appendChild( document.createElement( elmdef ) );
+                        if( input.type != 'radio' ){ input.type = 'radio'; }
+                        input.id = "input_" + (i+1) + "_" + j;
+                        input.name = "name";
+                        input.value = options[j];
+                        var label = div.appendChild( document.createElement( "label" ) );
+                        label.htmlFor = input.id;
+                        label.id = "label_" + input.id;
+                        label.className = "radio-label";
+                        label.style.display = "inline";
+                        label.innerHTML = options[j];
+                    }
+                }
+                if( sessionLogonCustomizations[i].trim() != "" ){
+                    var j = 0;
+                    while( ( radio = document.getElementById("input_" + (i+1) + "_" + j++ ) ) !== null ){ // what number
+                        if( typeof sessionLogonCustomizationPairs[i][ radio.value ] != "undefined" ){
+                            document.getElementById("label_input_" + (i+1) + "_" + (j-1) ).innerHTML = sessionLogonCustomizationPairs[i][ radio.value ];
+                        }
+                    }
+                }
+                var anyChecked = false, j = 0;
+                while( initValue != "" && ( radio = document.getElementById("input_" + (i+1) + "_" + j++ ) ) !== null ){
+                    radio.checked = ( radio.value == initValue );
+                    if( radio.checked ){ anyChecked = true; }
+                }
+                if( !anyChecked && ( radio = document.getElementById("input_" + (i+1) + "_" + 0 ) ) !== null ){
+                    radio.checked = true;
+                }
+            }else if( inpx !== null && inpx.tagName.toUpperCase() == "INPUT" && inpx.type.toUpperCase() == "CHECKBOX" ){
+                if( inpx.disabled && finitvalues[i] != "" ){
+                    inpx.checked = true;
+                }else if( !inpx.disabled && inpx.value == "" ){
+                    inpx.value = "1";
+                }
+            }
+        }
+    --></script>
+    </td>
+    <td id="main_table_image_cell"><img src="/public/images/my/tr.gif"></td></tr>
+</table>
+
+<script language="JavaScript" src="https://cdn.example.com/websites/globalapm/js/shared.js" ></script>
+<script language="JavaScript" src="https://cdn.example.com/websites/globalapm/js/shared-mfapage.js" ></script>
+<script language="JavaScript" src="https://cdn.example.com/websites/globalapm/js/shared-webtop.js" ></script>
+<script>
+    if (typeof(mfamethod) != 'undefined' && mfamethod != null)
+    {
+        // Check for browser support of event handling capability
+               if (window.addEventListener)
+               window.addEventListener("load", customizemfapage(), false);
+               else if (window.attachEvent)
+               window.attachEvent("onload", customizemfapage);
+               else window.onload = customizemfapage();
+    }
+    if (typeof(updatewebtopdiv) != 'undefined' && updatewebtopdiv != null)
+    {
+        // Check for browser support of event handling capability
+               if (window.addEventListener)
+               window.addEventListener("load", customizewebtop(), false);
+               else if (window.attachEvent)
+               window.attachEvent("onload", customizewebtop);
+               else window.onload = customizewebtop();
+    }
+</script>
+<div id="page_footer"><div>Protected using F5 APM. All rights reserved.</div></div>
+<script type="text/javascript">
+    function F5_include_JS(jsFile)
+    {
+        document.write('<script type="text/javascript" src="'
+                         + jsFile + '"></scr' + 'ipt>'); // <-- IE7 workaround
+    }
+
+    if ("" == "1") {
+        var f5VirtualKeyboardMove = ( "" == 1 ? "1" : "0" );
+        var f5VirtualKeyboardForceBlur = ( "" == 1 ? "0" : "1" );
+        function VirtualKeyboard_CustomizedMessageTextGet()
+{
+  return 'Virtual Keyboard';
+}
+
+function VirtualKeyboard_CustomizedHideTextGet()
+{
+  return 'Hide Keyboard';
+}
+
+        var f5VirtualKeyboardMessageText =  VirtualKeyboard_CustomizedMessageTextGet();
+        var f5VirtualKeyboardHideText =  VirtualKeyboard_CustomizedHideTextGet();
+        F5_include_JS('/public/include/js/vk/f5_vk.js');
+    }
+</script>
+<div id="MessageDIV" class="inspectionHostDIVSmall"></div>
+<div id="formHeaderSoftToken" style="overflow: hidden; visibility: hidden; height: 0; width: 0;"></div>
+</body>
+</html>
\ No newline at end of file
diff --git a/pkg/provider/f5apm/example/mfapage.html b/pkg/provider/f5apm/example/mfapage.html
new file mode 100644
index 0000000..8b6576d
--- /dev/null
+++ b/pkg/provider/f5apm/example/mfapage.html
@@ -0,0 +1,786 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<meta http-equiv="pragma" content="no-cache">
+<meta http-equiv="cache-control" content="no-cache">
+<title>federate.example.com</title>
+<link rel="stylesheet" type="text/css" HREF="/public/include/css/apm.css">
+<script language="JavaScript" src="/public/include/js/session_check.js?v=13" ></script>
+<script language="JavaScript" src="/public/include/js/agent_common.js" ></script>
+<script language="JavaScript" src="/public/include/js/web_host.js" ></script>
+<script language="javascript">
+<!--
+if(!String.prototype.trim){ String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); }; }
+
+/////////////////////////////////////////////////////////////////////////
+// function to return the customized screen when session expiration
+// is detected on the client-side
+/////////////////////////////////////////////////////////////////////////
+function SessionExpired_CustomizedScreenGet()
+{
+  return '<br><h3>Session Expired/Timeout</h3><br>Due to user inactivity, your session expired. Click the following link to start a new session.<br><br><table id="IHoptions"><tr><td><a href="[SESSION_RESTART_URL]" class="option"><img src="/public/images/sq-positive.gif" ></a><a href="[SESSION_RESTART_URL]">Start a new session</a></td></tr></table>';
+}
+
+var globalRestartOnSubmit = false;
+
+function sessionTimedOut()
+{
+    // restart session automatically upon "submit" for edge-like clients if logon form is compatibile
+    if (externalWebHost.hasWebLogonResetSession() && logonFormCompatible) {
+       globalRestartOnSubmit = true;
+    }
+    else {
+       // display session expired msg for non-compatible forms or
+       // non-edge clients
+       sessionTimeout.showSplashLayer("MessageDIV", SessionExpired_CustomizedScreenGet());
+    }
+}
+
+/**
+ * Place the focus and init the form fields
+ */
+
+if(self != top) { top.location = self.location; }
+window.onerror=function(){ return function(){ return; } }
+
+//--------------------------------------------------------------------
+var doAutoSubmit = true;
+
+var globalFormId = 'auth_form';
+var globalTableId = 'credentials_table';
+var globalSubmitTrId = 'submit_row';
+
+var globalSavePasswordCheckbox = null;
+var logonFormCompatible = false;
+
+var softTokenInput = null;
+var softTokenFieldId = "";
+var softTokenState = "";
+var softTokenNewPIN = "";
+var softTokenPINAutoPopulate = false;
+if( !softTokenState ) {
+    softTokenState = "SECURID_AUTH_STATE_START";}
+
+    var challengeMode = false;
+
+function getInputField(fieldId)
+{
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return null;
+    }
+
+    if(fieldId == null || fieldId == ""){
+        return null;
+    }
+
+    var inputs = form.getElementsByTagName("input");
+    for( var i=0; i<inputs.length; i++ ){
+        if(  inputs[i].name == fieldId){
+            return inputs[i];
+        }
+    }
+    return null;
+}
+
+function getSoftTokenInput()
+{
+    return getInputField(softTokenFieldId);
+}
+
+function getUsernameInput()
+{
+    return getInputField("username");
+}
+
+function getPasswordInput()
+{
+    return getInputField("password");
+}
+
+function edgeClientSoftTokenSupport()
+{
+    try {
+        return externalWebHost.hasWebLogonSoftTokenSupport();
+    } catch(e) {}
+    return false;
+}
+
+function getSoftTokenPrompt()
+{
+    if ( softTokenFieldId != "" && edgeClientSoftTokenSupport()) {
+        var div = document.getElementById("formHeaderSoftToken");
+        if (div)  {
+            return div.innerHTML;
+        }
+    }
+    return  null;
+}
+
+function setSoftTokenChallengeResponse(challengeResponse)
+{
+    var challengeElements = document.getElementsByName('_F5_challenge');
+    if((challengeElements.length == 0) || (null == challengeElements[0])) {
+        return;
+    }
+
+    var challengeElement = challengeElements[0];
+    if(challengeElement.tagName.toLowerCase() == 'select') {
+        var challengeOptions = challengeElement.options;
+        if(null == challengeOptions){
+            return;
+        }
+        for (var i = 0, optionsLength = challengeOptions.length; i < optionsLength; i++) {
+            if (challengeOptions[i].value == challengeResponse) {
+                challengeElement.selectedIndex = i;
+                break;
+            }
+        }
+    }else if(challengeElement.tagName.toLowerCase() == 'input') {
+        challengeElement.value = challengeResponse;
+    }
+}
+
+function OnSubmitEdgeRSASoftToken()
+{
+    var support = edgeClientSoftTokenSupport();
+    if( true != support ) {
+        return;
+    }
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+    var inputs = form.getElementsByTagName("input");
+
+    var hiddenInput = document.createElement("input");
+    hiddenInput.setAttribute("type", "hidden");
+        hiddenInput.setAttribute("name", "softTokenError");
+    //append to form element that you want .
+    form.appendChild(hiddenInput);
+
+    try{
+        if(softTokenInput != null) { //normal mode
+            // if the client cannot request soft-token PIN,
+            // ask user to enter it in the logon page and pass the value to the client
+            // Otherwise, the client already has the PIN
+            if(!softTokenPINAutoPopulate) {
+                externalWebHost.setWebLogonSoftTokenPIN(softTokenInput.value);
+            }
+            var passcode = externalWebHost.getWebLogonSoftTokenPasscode();
+            if( passcode ) {
+                softTokenInput.value = passcode;
+            }
+        } else if( challengeMode ) { //challenge mode
+            // If the client can provide response to the challenge, use it instead of user input
+            if(externalWebHost.hasWeblogonSoftTokenChallengeResponse()){
+                var challengeResponse = externalWebHost.getWeblogonSoftTokenChallengeResponse();
+                if(null != challengeResponse){
+                     setSoftTokenChallengeResponse(challengeResponse);
+                }
+            }else{
+                // Request user input by default
+                externalWebHost.setWebLogonSoftTokenPIN(inputs[0].value);
+
+                var passcode = externalWebHost.getWebLogonSoftTokenPasscode();
+                if( passcode ) {
+                    inputs[0].value = passcode;
+                }
+            }
+
+        }
+        hiddenInput.value = externalWebHost.getWebLogonSoftTokenError();
+    } catch(e) { }
+
+    return;
+}
+
+// check whether logon form is compatible with the client for auto-population and auto-submission
+// only "username", "password" and soft token field (the name is stored in softTokenFieldId ) are supported
+// fields can be configured in either order, but can only be text, password or checkbox types
+// form is not considered compatible if it contains any additional fields not supported by the client
+// The form is compatible even if it contains a subset of the supported fields
+// In this case the client will auto-populate only those fields
+function getFormCompatibility()
+{
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return false;
+    }
+    // check if form suites
+    var inputs = [];
+    var inputsTemp = form.getElementsByTagName("input");
+    // filter submit, reset, hidden and little green men
+    for( var i=0; i<inputsTemp.length; i++ ){
+        if( inputsTemp[i].type == "text" || inputsTemp[i].type == "password"){
+            inputs[ inputs.length ] = inputsTemp[i];
+        }
+    }
+
+    var softTokenSupported = edgeClientSoftTokenSupport() && (null != softTokenInput);
+    // Check if there are any custom fields that are not supported by Edge Client for auto-population and auto-submission
+    for( var i=0; i<inputs.length; i++ ){
+       if((inputs[i].type == "text" && inputs[i].name == "username") ||
+          (inputs[i].type == "password" && inputs[i].name == "password") ||
+          (softTokenSupported && inputs[i].type == "password" && inputs[i].name == softTokenInput.name)){
+               continue;
+       }else {
+          return false;
+       }
+    }
+    return true;
+}
+
+function setOrigUriLink()
+{
+    var params = parseQueryParams(window.location.search.substr(1));
+    if (!params.hasOwnProperty('ORIG_URI')) {
+        return;
+    }
+
+    var credTable = document.getElementById('credentials_table');
+    if (credTable == null) {
+        return;
+    }
+    var tBody = credTable.tBodies[0];
+    if (tBody == null) {
+        return;
+    }
+
+    var trTag = document.createElement("TR");
+    tBody.insertBefore(trTag, tBody.children[tBody.children.length - 1]);
+
+    var tdTag = document.createElement("TD");
+    tdTag.setAttribute("class", "credentials_table_unified_cell");
+    tdTag.setAttribute("colspan", "2");
+    trTag.appendChild(tdTag);
+
+    var tdText = document.createTextNode("Click here if already logged in ");
+    tdTag.appendChild(tdText);
+
+    var origUri = atob(decodeURIComponent(params['ORIG_URI']));
+    var tdLink = document.createElement("A");
+    tdLink.innerHTML = origUri;
+    tdLink.setAttribute("href", origUri);
+    tdTag.appendChild(tdLink);
+
+}
+
+function OnLoad()
+{
+    var header = document.getElementById("credentials_table_header");
+    var softTokenHeaderStr = getSoftTokenPrompt();
+    if ( softTokenHeaderStr ) {
+        header.innerHTML = softTokenHeaderStr;
+    }
+    setFormAttributeByQueryParams("auth_form", "action", "/subsession_logon_submit.php3");
+    setFormAttributeByQueryParams("v2_original_url", "href", "/subsession_logon_submit.php3");
+    setOrigUriLink();
+
+    // check if form suites
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+    // check if form suites
+    var inputs = form.getElementsByTagName("input");
+    // filter submit, reset, hidden and little green men
+    for( var i=0; i<inputs.length; i++ ){
+        if( ( inputs[i].type == "text" || inputs[i].type == "password" ) && inputs[i].value == "" ){
+            inputs[i].focus();
+            if (inputs[i].type == "password") {
+                window.setTimeout( function(elem){ return function(){ elem.blur(); elem.focus(); } }(inputs[i]) , 266 );
+            }
+            return;
+        }
+    }
+}
+
+function disableSubmit(form)
+{
+    // disable!
+    var inputs = form.getElementsByTagName( "input" );
+    for( var i=0; i<inputs.length; i++ ){
+        if( inputs[i].type == "submit" || inputs[i].type == "reset" ){
+            inputs[i].disabled = true;
+        }
+    }
+
+    return true;
+}
+
+//This function is called from Edge Client. Update setWeblogonCallbacks call if renamed
+function challengeAutoSubmit()
+{
+    if(!challengeMode) {
+       return false;
+    }
+
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+       return false;
+    }
+
+    disableSubmit(form);
+
+    form.onsubmit();
+    form.submit();
+    return true;
+}
+
+//This function is called from Edge Client. Update setWeblogonCallbacks call if renamed
+function weblogonAutoSubmit()
+{
+      if(!logonFormCompatible){
+          return false;
+      }
+
+      var form = document.getElementById( globalFormId );
+      if( form == null ){
+          return false;
+      }
+
+      // autosubmit check
+      if(externalWebHost.hasWebLogonAutoLogon() && externalWebHost.getWebLogonAutoLogon() && doAutoSubmit){
+
+         disableSubmit(form);
+
+         form.onsubmit();
+         form.submit();
+         return true;
+      }else{
+        return false;
+      }
+}
+
+
+// support for autologon from Client API
+function checkExternalAddCheckbox()
+{
+    var checkbox_txt = 'Save Password';
+
+    if( !logonFormCompatible ){
+        try {
+            if (externalWebHost.hasWebLogonNotifyUser()){
+                externalWebHost.WebLogonNotifyUser();
+            }
+        } catch(e){};
+        return;
+    }
+
+    // find form
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+
+    // find table
+    var table = document.getElementById( globalTableId );
+    if( table == null ){
+        return;
+    }
+
+    // find tr
+    var submitTr = document.getElementById( globalSubmitTrId );
+    if( submitTr == null ){
+        return;
+    }
+
+    try {
+        if(externalWebHost.isAvailable()){
+            // push values
+            var usernameInput = getUsernameInput();
+            if( null != usernameInput && externalWebHost.hasWebLogonUserName() && usernameInput.value == ""){
+                usernameInput.value = externalWebHost.getWebLogonUserName();
+            }
+
+            var passwordInput = getPasswordInput();
+                        var allowSavingPassword = ((null != passwordInput) && (passwordInput.name != softTokenFieldId));
+            //don't populate RSA SecurID token field with the cached password
+            if(allowSavingPassword) {
+                if( externalWebHost.hasWebLogonPassword() && (passwordInput.value == "")){
+                     passwordInput.value =  externalWebHost.getWebLogonPassword();
+                }
+            }else{
+                doAutoSubmit = false;
+            }
+
+            // push data to cells
+            if(allowSavingPassword && externalWebHost.isWebLogonSavePasswordAvailable()){
+                // right - text
+
+                // create cells
+                var newTr = table.insertRow( submitTr.rowIndex );
+                var leftTd = newTr.insertCell( 0 );
+                var rightTd = newTr.insertCell( 1 );
+
+                leftTd.className = "credentials_table_label_cell";
+                rightTd.className = "credentials_table_field_cell credentials_table_field_checkbox_fix";
+
+                rightTd.innerHTML = checkbox_txt;
+                // left - checkbox
+                globalSavePasswordCheckbox = document.createElement("input");
+                globalSavePasswordCheckbox.type = "checkbox";
+                globalSavePasswordCheckbox.className = "credentials_input_checkbox";
+                globalSavePasswordCheckbox.value = 1;
+                globalSavePasswordCheckbox = leftTd.appendChild( globalSavePasswordCheckbox );
+                globalSavePasswordCheckbox.checked = externalWebHost.getWebLogonSavePasswordChecked();
+                if( globalSavePasswordCheckbox.autocomplete ) {
+                    globalSavePasswordCheckbox.autocomplete = "off";
+                }
+            }
+
+            // autosubmit if possible
+            if(weblogonAutoSubmit()){
+               return;
+            }
+        }
+    } catch (e) { }
+}
+
+function OnSubmit()
+{
+    // find form
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return;
+    }
+    try{
+      if( externalWebHost.isAvailable() ){
+         // pass weblogon credentials back to the client
+         if( logonFormCompatible ){
+              var usernameInput = getUsernameInput();
+              if( null != usernameInput && externalWebHost.hasWebLogonUserName() ){
+                  externalWebHost.setWebLogonUserName(usernameInput.value);
+              }
+
+              var passwordInput = getPasswordInput();
+              if( null != passwordInput && externalWebHost.hasWebLogonPassword() ){
+                  externalWebHost.setWebLogonPassword(passwordInput.value);
+              }
+              // pass user decision to save the password back to the client
+              if( externalWebHost.hasWebLogonSavePasswordChecked() ){
+                  externalWebHost.setWebLogonSavePasswordChecked((null != globalSavePasswordCheckbox) && globalSavePasswordCheckbox.checked);
+              }
+          }
+          if (softTokenFieldId != "") {
+              OnSubmitEdgeRSASoftToken();
+          }
+      }
+    } catch(e) { }
+
+    return;
+}
+
+function verifyNewPassword()
+{
+    var form = document.getElementById( globalFormId );
+    if( form == null ){
+        return true;
+    }
+
+    var inputs = form.getElementsByTagName("input");
+    if( inputs.length >= 2 && inputs[0].name == "_F5_challenge" && inputs[0].type == "password" && inputs[1].name == "_F5_verify_password" && inputs[1].type == "password" ){
+        if( inputs[0].value != inputs[1].value ){
+            alert("Password and confirmation do not match.");
+            inputs[0].focus();
+            return false;
+        } else {
+            // Not sending the second field.
+            inputs[1].disabled = true;
+
+            try{
+                  if( externalWebHost.hasWebLogonPassword() ){
+                      externalWebHost.setWebLogonPassword(inputs[0].value);
+                  }
+            } catch(e) { }
+        }
+    }
+    return true;
+}
+
+function masterSubmit(form)
+{
+    if( !verifyNewPassword() /* || ... */){
+        return false;
+    }
+
+    OnSubmit(); // this required by edge
+
+    if (globalRestartOnSubmit) {
+        try {
+            var usernameInput = getUsernameInput();
+            var passwordInput = getPasswordInput();
+            externalWebHost.setWebLogonAutoLogon((usernameInput != null ) && ("" != usernameInput.value) && (null != passwordInput) && ("" != passwordInput.value));
+        }
+        catch (e) {}
+        // restart session
+        externalWebHost.webLogonResetSession();
+        return false;
+    }
+
+    disableSubmit(form);
+    return true;
+}
+//-->
+</script>
+</head>
+
+<body onload="OnLoad()">
+
+
+<!-- CSS Order Is Important, order should be Shared-UI, Shared-Logos, Custom-Skin -->
+<link id="customcss" rel="stylesheet" type="text/css" href="https://cdn.example.com/websites/globalapm/css/shared-logos-text.css"> 
+<link id="customcss" rel="stylesheet" type="text/css" href="https://cdn.example.com/websites/globalapm/css/shared-ui.css"> 
+<link id="customcss" rel="stylesheet" type="text/css" href="https://cdn.example.com/websites/globalapm/css/federate-skin.css">
+
+<meta name="viewport" id="viewport" content="initial-scale=1.0" />       
+<meta name="robots" context="noindex,nofollow">
+<script language="javascript">
+//orientation toolbox
+function setViewport(){
+    document.getElementById("viewport").setAttribute('content', Math.abs(window.orientation) == 90 ? 'width=device-height, initial-scale=1' : 'width=device-width, initial-scale=1.0' );
+}
+setViewport();
+window.onorientationchange = function() {
+    setViewport();
+}    
+</script>
+<table id="page_header">
+  <tr >
+    <td id="header_leftcell"><img src='/public/images/my/flogo.png'/></td>             
+    <td id="new_header_leftcell"><img src='https://cdn.example.com/websites/globalapm/logos/logo-federate.svg'/></td>             
+    <td id="header_rightcell"></td>
+  </tr>
+  <tr>
+     <td></td>
+     <td id="maxTimeoutTd"><span id="maxTimeoutTag" title=""></span></td>
+  </tr>
+</table>
+<noscript>
+<div id="noscript_warning_red">JavaScript is not enabled. Please enable JavaScript in your browser or contact your system administrator for assistance.</div>
+<div id="noscript_warning_newsession">To open a new session, please  <A href="/">click here.</A></div>
+</noscript>
+
+<table id="main_table" class="logon_page">
+<tr>
+        <td id="main_table_info_cell">
+    <form id="auth_form" name="e1" method="post" onsubmit="javascript: return masterSubmit(this);" autocomplete="off">
+    <table id="credentials_table">
+    <tr>
+        <td colspan=2 id="credentials_table_header" ><span class="info-text">Please select your preferred method for Multi-Factor Authentication</span>
+<input type="hidden" name="mfa_retry" id="mfa_retry" value="" /></td>
+    </tr>
+    <tr>
+        <td colspan=2 id="credentials_table_postheader" ></td>
+    </tr>
+    <tr>
+        <td colspan=2 class="credentials_table_unified_cell" ><label for='input_1' id='label_input_1'>Username</label><input type='text' name='username' class='credentials_input_text_disabled' value='user_name' id='input_1' autocomplete='off' autocapitalize='off' disabled/></td>
+    </tr>
+    <tr>
+        <td colspan=2 class="credentials_table_unified_cell" ><label for='input_3' id='label_input_3'>Multi-Factor Authentication Method</label><select name='mfamethod' id='input_3' class='credentials_input_select' ><option value="push" >push</option><option value="token" >token</option></select></td>
+    </tr>
+    <tr>
+        <td colspan=2 class="credentials_table_unified_cell" ><label for='input_4' id='label_input_4'>One-Time Passcode</label><input type='text' name='mfatoken' class='credentials_input_text' value='' id='input_4' autocomplete='off' autocapitalize='off' /></td>
+    </tr>
+    <tr id="submit_row">
+        <td class="credentials_table_unified_cell"><input type=submit class="credentials_input_submit" value="Validate"></td>
+    </tr>
+    <tr>
+        <td colspan=2 id="credentials_table_footer" ></td>
+    </tr>
+    </table>
+    <input type=hidden name="vhost" value="standard">
+    </form>
+    <script language="javascript"><!--//
+        softTokenInput = getSoftTokenInput();
+        logonFormCompatible = getFormCompatibility();
+        // Pass method names to the client for auto-submit triggered by the client
+        externalWebHost.setWeblogonCallbacks(
+             "weblogonAutoSubmit();",
+             "challengeAutoSubmit();"
+        );
+
+        // Check if the softTokenInput exists or in case of RSA challenge mode
+        if((softTokenFieldId != "") && (null != softTokenInput || challengeMode ) && edgeClientSoftTokenSupport()){
+            externalWebHost.setWebLogonSoftTokenPrompt(getSoftTokenPrompt());
+            externalWebHost.setWebLogonSoftTokenState(softTokenState);
+        }
+
+        // Check if the client can ask user for soft-token PIN
+        if((softTokenFieldId != "") && (softTokenInput != null)
+               && edgeClientSoftTokenSupport()
+               && externalWebHost.canRequestWeblogonSoftTokenPIN()){
+            softTokenInput.readOnly = true;
+            softTokenPINAutoPopulate = true;
+            // No need to request input of RSA PIN, as the client can show PIN input UI if needed,
+            // make the field read-only
+            // set bogus value to display
+            softTokenInput.value = "********";
+        }
+
+        checkExternalAddCheckbox();
+        setTimeout(function(){ window.sessionTimeout = new APMSessionTimeout(sessionTimedOut); }, 200);
+
+        function dontChangePasswordClick(){
+            var checkbox = document.getElementById("dont_change_password_checkbox");
+            var password = document.getElementById("input_1");
+            var verify = document.getElementById("input_2");
+            if( checkbox.checked ){
+                password.value = "";
+                password.disabled = true;
+                verify.value = "";
+                verify.disabled = true;
+            }else{
+                password.disabled = false;
+                verify.disabled = false;
+            }
+        }
+
+        var finitvalues=['user_name','','','',''];
+        var sessionLogonCustomizations = ["","","push=>Safenet Push Notification;token=>Safenet One-Time Passcode","",""];
+        var sessionLogonCustomizationPairs = [[], [], [], [], []];
+        for( var i=0; i<sessionLogonCustomizations.length; i++ ){
+            var pairs = ( sessionLogonCustomizations[i].indexOf(";") == -1 ? [ sessionLogonCustomizations[i] ] : sessionLogonCustomizations[i].trim().split(";") );
+            for( var j=0; j<pairs.length; j++ ){
+                if( pairs[j].indexOf("=>") != -1 ){
+                    var pair = pairs[j].split("=>");
+                    sessionLogonCustomizationPairs[i][pair[0]] = pair[1];
+                }
+            }
+        }
+
+        var sessionLogonValuesets = [ "", "", "", "", "" ];
+        for( var i=0; i<sessionLogonValuesets.length; i++ ){
+            var inpx = document.getElementById("input_" + (i+1) );
+            if( inpx !== null && inpx.tagName.toUpperCase() == "SELECT" ){
+                if( sessionLogonValuesets[i].trim() != "" ){
+                    inpx.options.length = 0;
+                    var options = sessionLogonValuesets[i].trim().split(";");
+                    for( var j=0; j<options.length; j++ ){
+                        inpx.options.add( new Option( options[j], options[j] ) );
+                    }
+                }
+                if( sessionLogonCustomizations[i].trim() != "" ){
+                    for( var j=0; j<inpx.options.length; j++ ){
+                        if( typeof sessionLogonCustomizationPairs[i][ inpx.options[j].value ] != "undefined" ){
+                            inpx.options[j].text = sessionLogonCustomizationPairs[i][ inpx.options[j].value ];
+                        }
+                    }
+                }
+                // set value
+                for( var j=0; j<inpx.options.length; j++ ){
+                    if( inpx.options[j].value == finitvalues[i] ){
+                        inpx.value = finitvalues[i];
+                        break;
+                    }
+                }
+            }else if( document.getElementById("input_" + (i+1) + "_0" ) ){ // RADIO
+                var initValue = finitvalues[i];
+                var radio = null;
+                var name = document.getElementById("input_" + (i+1) + "_0" ).name;
+                if( sessionLogonValuesets[i].trim() != "" ){
+                    var parent = document.getElementById( "label_input_" + (i+1) ).parentNode;
+                    while( parent.childNodes.length > 1 ){
+                        parent.removeChild( parent.lastChild );
+                    }
+                    var options = sessionLogonValuesets[i].trim().split(";");
+                    for( var j=0; j<options.length; j++ ){
+                        var div = parent.appendChild( document.createElement( "div" ) );
+                        var elmdef = document.all && navigator.userAgent.match(/MSIE (\d+)/)[1] < 9 ? "<input type='radio'>" : "input";
+                        var input = div.appendChild( document.createElement( elmdef ) );
+                        if( input.type != 'radio' ){ input.type = 'radio'; }
+                        input.id = "input_" + (i+1) + "_" + j;
+                        input.name = "name";
+                        input.value = options[j];
+                        var label = div.appendChild( document.createElement( "label" ) );
+                        label.htmlFor = input.id;
+                        label.id = "label_" + input.id;
+                        label.className = "radio-label";
+                        label.style.display = "inline";
+                        label.innerHTML = options[j];
+                    }
+                }
+                if( sessionLogonCustomizations[i].trim() != "" ){
+                    var j = 0;
+                    while( ( radio = document.getElementById("input_" + (i+1) + "_" + j++ ) ) !== null ){ // what number
+                        if( typeof sessionLogonCustomizationPairs[i][ radio.value ] != "undefined" ){
+                            document.getElementById("label_input_" + (i+1) + "_" + (j-1) ).innerHTML = sessionLogonCustomizationPairs[i][ radio.value ];
+                        }
+                    }
+                }
+                var anyChecked = false, j = 0;
+                while( initValue != "" && ( radio = document.getElementById("input_" + (i+1) + "_" + j++ ) ) !== null ){
+                    radio.checked = ( radio.value == initValue );
+                    if( radio.checked ){ anyChecked = true; }
+                }
+                if( !anyChecked && ( radio = document.getElementById("input_" + (i+1) + "_" + 0 ) ) !== null ){
+                    radio.checked = true;
+                }
+            }else if( inpx !== null && inpx.tagName.toUpperCase() == "INPUT" && inpx.type.toUpperCase() == "CHECKBOX" ){
+                if( inpx.disabled && finitvalues[i] != "" ){
+                    inpx.checked = true;
+                }else if( !inpx.disabled && inpx.value == "" ){
+                    inpx.value = "1";
+                }
+            }
+        }
+    --></script>
+    </td>
+    <td id="main_table_image_cell"><img src="/public/images/my/tr.gif"></td></tr>
+</table>
+
+<script language="JavaScript" src="https://cdn.example.com/websites/globalapm/js/shared.js" ></script>
+<script language="JavaScript" src="https://cdn.example.com/websites/globalapm/js/shared-mfapage.js" ></script>
+<script language="JavaScript" src="https://cdn.example.com/websites/globalapm/js/shared-webtop.js" ></script>
+<script>
+    if (typeof(mfamethod) != 'undefined' && mfamethod != null)
+    {
+        // Check for browser support of event handling capability
+               if (window.addEventListener)
+               window.addEventListener("load", customizemfapage(), false);
+               else if (window.attachEvent)
+               window.attachEvent("onload", customizemfapage);
+               else window.onload = customizemfapage();
+    }
+    if (typeof(updatewebtopdiv) != 'undefined' && updatewebtopdiv != null)
+    {
+        // Check for browser support of event handling capability
+               if (window.addEventListener)
+               window.addEventListener("load", customizewebtop(), false);
+               else if (window.attachEvent)
+               window.attachEvent("onload", customizewebtop);
+               else window.onload = customizewebtop();
+    }
+</script>
+<div id="page_footer"><div>Protected using F5 APM. All rights reserved.</div></div>
+<script type="text/javascript">
+    function F5_include_JS(jsFile)
+    {
+        document.write('<script type="text/javascript" src="'
+                         + jsFile + '"></scr' + 'ipt>'); // <-- IE7 workaround
+    }
+
+    if ("" == "1") {
+        var f5VirtualKeyboardMove = ( "" == 1 ? "1" : "0" );
+        var f5VirtualKeyboardForceBlur = ( "" == 1 ? "0" : "1" );
+        function VirtualKeyboard_CustomizedMessageTextGet()
+{
+  return 'Virtual Keyboard';
+}
+
+function VirtualKeyboard_CustomizedHideTextGet()
+{
+  return 'Hide Keyboard';
+}
+
+        var f5VirtualKeyboardMessageText =  VirtualKeyboard_CustomizedMessageTextGet();
+        var f5VirtualKeyboardHideText =  VirtualKeyboard_CustomizedHideTextGet();
+        F5_include_JS('/public/include/js/vk/f5_vk.js');
+    }
+</script>
+<div id="MessageDIV" class="inspectionHostDIVSmall"></div>
+<div id="formHeaderSoftToken" style="overflow: hidden; visibility: hidden; height: 0; width: 0;"></div>
+</body>
+</html>
diff --git a/pkg/provider/f5apm/f5apm.go b/pkg/provider/f5apm/f5apm.go
new file mode 100644
index 0000000..e72cfbf
--- /dev/null
+++ b/pkg/provider/f5apm/f5apm.go
@@ -0,0 +1,270 @@
+package f5apm
+
+import (
+	"bytes"
+	"encoding/base64"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/PuerkitoBio/goquery"
+
+	"github.com/versent/saml2aws/pkg/cfg"
+	"github.com/versent/saml2aws/pkg/creds"
+	"github.com/versent/saml2aws/pkg/dump"
+	"github.com/versent/saml2aws/pkg/prompter"
+
+	"github.com/pkg/errors"
+	"github.com/versent/saml2aws/pkg/provider"
+
+	"github.com/sirupsen/logrus"
+)
+
+var logger = logrus.WithField("provider", "f5apm")
+
+//Client client for F5 APM
+type Client struct {
+	client   *provider.HTTPClient
+	policyID string
+}
+
+// New create new F5 APM client
+func New(idpAccount *cfg.IDPAccount) (*Client, error) {
+
+	tr := provider.NewDefaultTransport(idpAccount.SkipVerify)
+	client, err := provider.NewHTTPClient(tr)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error building HTTP client")
+	}
+	return &Client{client: client, policyID: idpAccount.ResourceID}, nil
+}
+
+// Authenticate logs into F5 APM and returns a SAML response
+func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
+	logger.Debug("Get Login Form")
+	logger.Debugf("Login URL: %s", loginDetails.URL)
+	logger.Debugf("Login Username: %s", loginDetails.Username)
+	authForm, err := ac.getLoginForm(loginDetails)
+	if err != nil {
+		return "", errors.Wrap(err, "Error getting login form IDP")
+	}
+
+	// Post username/password
+	logger.Debug("Post UP Login Form")
+	debugAuthForm(authForm)
+
+	upData, err := ac.postLoginForm(loginDetails, authForm)
+	if err != nil {
+		return "", errors.Wrap(err, "Error submitting login form")
+	}
+
+	upDoc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(upData))
+	mfaFound, mfaMethods := containsMFAForm(upDoc)
+
+	// Prompt for MFA if needed
+	if mfaFound {
+		logger.Debug(mfaMethods)
+		mfaAuthForm := url.Values{}
+		var mfaToken string
+		mfaMethod, err := prompter.ChooseWithDefault("MFA Method", mfaMethods[0], mfaMethods)
+		if err != nil {
+			return "", errors.Wrap(err, "Error selecting MFA method")
+		}
+		switch mfaMethod {
+		case "token":
+			mfaToken = prompter.RequestSecurityCode("000000")
+		case "push":
+			mfaToken = ""
+		}
+		// Post mfatoken
+		mfaAuthForm.Add("mfatoken", mfaToken)
+		mfaAuthForm.Add("mfamethod", mfaMethod)
+		mfaAuthForm.Add("mfa_retry", "")
+		logger.Debug("Post Token Form")
+		debugAuthForm(mfaAuthForm)
+		_, err = ac.postLoginForm(loginDetails, mfaAuthForm)
+		if err != nil {
+			return "", errors.Wrap(err, "Error submitting MFA login form")
+		}
+	}
+
+	// Post to saml endpoint
+	logger.Debug("Get SAML Form")
+	samlAssertion, err := ac.getSAMLAssertion(loginDetails)
+	if err != nil {
+		return "", errors.Wrap(err, "Error getting saml assertion")
+	}
+	decodedAssertion, err := base64.StdEncoding.DecodeString(samlAssertion)
+	if err != nil {
+		return "", errors.Wrap(err, "Error decoding saml assertion")
+	}
+	if dump.ContentEnable() {
+		logger.Debugf("SAMLAssertion: %s", string(decodedAssertion))
+
+	}
+	return samlAssertion, nil
+}
+
+func (ac *Client) getSAMLAssertion(loginDetails *creds.LoginDetails) (string, error) {
+	req, err := http.NewRequest("GET", fmt.Sprintf("%s/saml/idp/res", loginDetails.URL), nil)
+
+	if err != nil {
+		return "", errors.Wrap(err, "Error building SAML assertion request")
+	}
+	debugHTTPRequest(ac, req)
+	// Don't urlencode query string - APM bug
+	req.URL.RawQuery = fmt.Sprintf("id=%s", ac.policyID)
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return "", errors.Wrap(err, "Error retrieving SAML assertion request")
+	}
+	debugHTTPResponse(ac, res)
+	samlData, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return "", errors.Wrap(err, "Error reading SAML asseration body")
+	}
+	var samlAssertion string
+	doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(samlData))
+	doc.Find("input").Each(func(i int, s *goquery.Selection) {
+		name, ok := s.Attr("name")
+		if !ok {
+			logger.Fatalf("Unable to locate IDP authentication")
+		}
+		if name == "SAMLResponse" {
+			val, ok := s.Attr("value")
+			if !ok {
+				logger.Fatalf("Unable to locate SAML assertion value")
+			}
+			samlAssertion = val
+		}
+	})
+	return samlAssertion, nil
+}
+
+func (ac *Client) getLoginForm(loginDetails *creds.LoginDetails) (url.Values, error) {
+	req, err := http.NewRequest("GET", loginDetails.URL, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error building get loging form request")
+	}
+	debugHTTPRequest(ac, req)
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error retrieving login form")
+	}
+	debugHTTPResponse(ac, res)
+
+	doc, err := goquery.NewDocumentFromReader(res.Body)
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to build document from response")
+	}
+	authForm := url.Values{}
+	doc.Find("input").Each(func(i int, s *goquery.Selection) {
+		name, ok := s.Attr("name")
+		if !ok {
+			return
+		}
+		lname := strings.ToLower(name)
+		if strings.Contains(lname, "username") {
+			authForm.Add(name, loginDetails.Username)
+		} else if strings.Contains(lname, "password") {
+			authForm.Add(name, loginDetails.Password)
+		} else {
+			val, ok := s.Attr("value")
+			if !ok {
+				return
+			}
+			authForm.Add(name, val)
+		}
+	})
+	return authForm, nil
+}
+
+func (ac *Client) postLoginForm(loginDetails *creds.LoginDetails, authForm url.Values) ([]byte, error) {
+	logger.Debug("Auth Post")
+
+	req, err := http.NewRequest("POST", fmt.Sprintf("%s/my.policy", loginDetails.URL), strings.NewReader(authForm.Encode()))
+	if err != nil {
+		return nil, errors.Wrap(err, "Error building authentication request")
+	}
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.00) Gecko/20100101 Firefox/65.0")
+	req.Header.Set("Accept", "*/*")
+
+	req.Header.Add("Referer", fmt.Sprintf("%s/my.policy", loginDetails.URL))
+	if authForm.Get("mfamethod") != "" {
+		req.AddCookie(&http.Cookie{Name: "f5cid00", Value: "token"})
+	}
+	debugHTTPRequest(ac, req)
+	res, err := ac.client.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error retrieving login form")
+	}
+	debugHTTPResponse(ac, res)
+
+	data, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error reading response body")
+	}
+	return data, nil
+}
+
+func debugAuthForm(vals url.Values) {
+	for key, values := range vals {
+		if strings.ToLower(key) == "password" {
+			values = []string{"XXXXXXXXX"}
+		}
+		logger.Debugf("%-20s %-18s: %-40s", "Auth Form:", key, strings.Join(values, ", "))
+	}
+}
+
+func debugHTTPRequest(ac *Client, req *http.Request) {
+	logger.Debug(dump.RequestString(req))
+	logger.Debug(req.URL)
+	for name, values := range req.Header {
+		logger.Debugf("%-20s %-18s: %-40s", fmt.Sprintf("%s Request Header:", req.Method), name, strings.Join(values, ", "))
+	}
+	for _, reqCookie := range ac.client.Jar.Cookies(req.URL) {
+		logger.Debugf("%-20s %-18s: %-40s %s", fmt.Sprintf("%s Request Cookie:", req.Method), reqCookie.Name, reqCookie.Value, reqCookie.Domain)
+	}
+
+}
+func debugHTTPResponse(ac *Client, res *http.Response) {
+	logger.Debug(dump.ResponseString(res))
+	logger.Debug(res.Request.URL)
+	for name, values := range res.Header {
+		logger.Debugf("%-20s %-18s: %-40s", fmt.Sprintf("%s Response Header:", res.Request.Method), name, strings.Join(values, ", "))
+	}
+	for _, resCookie := range ac.client.Jar.Cookies(res.Request.URL) {
+		logger.Debugf("%-20s %-18s: %-40s %s", fmt.Sprintf("%s Response Cookie:", res.Request.Method), resCookie.Name, resCookie.Value, resCookie.Domain)
+	}
+}
+
+func containsMFAForm(doc *goquery.Document) (bool, []string) {
+	containsMFA := false
+	var mfaMethods []string
+	// Look for a form input ID named "mfa_retry"
+	doc.Find("input").Each(func(i int, s *goquery.Selection) {
+		id, _ := s.Attr("id")
+		if strings.Contains(id, "mfa_retry") {
+			containsMFA = true
+		}
+	})
+	doc.Find("select").Each(func(i int, s *goquery.Selection) {
+		name, _ := s.Attr("name")
+		if strings.Contains(name, "mfamethod") {
+			s.Find("option").Each(func(i int, opt *goquery.Selection) {
+				option, _ := opt.Attr("value")
+				logger.Debugf("MFA options: %s", option)
+				mfaMethods = append(mfaMethods, option)
+			})
+		}
+	})
+	if len(mfaMethods) == 0 {
+		return false, nil
+	}
+	logger.Debugf("MFA Form: '%#v'", containsMFA)
+	logger.Debugf("MFA Methods: '%#v'", mfaMethods)
+	return containsMFA, mfaMethods
+}
diff --git a/saml2aws.go b/saml2aws.go
index 1c23eb6..d0400d2 100644
--- a/saml2aws.go
+++ b/saml2aws.go
@@ -8,6 +8,7 @@ import (
 	"github.com/versent/saml2aws/pkg/creds"
 	"github.com/versent/saml2aws/pkg/provider/adfs"
 	"github.com/versent/saml2aws/pkg/provider/adfs2"
+	"github.com/versent/saml2aws/pkg/provider/f5apm"
 	"github.com/versent/saml2aws/pkg/provider/googleapps"
 	"github.com/versent/saml2aws/pkg/provider/jumpcloud"
 	"github.com/versent/saml2aws/pkg/provider/keycloak"
@@ -35,6 +36,7 @@ var MFAsByProvider = ProviderList{
 	"GoogleApps": []string{"Auto"},                                       // automatically detects ToTP
 	"Shibboleth": []string{"Auto"},
 	"PSU":        []string{"Auto"},
+	"F5APM":      []string{"Auto"},
 }
 
 // Names get a list of provider names
@@ -135,6 +137,12 @@ func NewSAMLClient(idpAccount *cfg.IDPAccount) (SAMLClient, error) {
 			return nil, fmt.Errorf("Invalid MFA type: %v for %v provider", idpAccount.MFA, idpAccount.Provider)
 		}
 		return psu.New(idpAccount)
+	case "F5APM":
+		if invalidMFA(idpAccount.Provider, idpAccount.MFA) {
+			return nil, fmt.Errorf("Invalid MFA type: %v for %v provider", idpAccount.MFA, idpAccount.Provider)
+		}
+		return f5apm.New(idpAccount)
+
 	default:
 		return nil, fmt.Errorf("Invalid provider: %v", idpAccount.Provider)
 	}


Test patch:
diff --git a/pkg/provider/f5apm/f5apm_test.go b/pkg/provider/f5apm/f5apm_test.go
new file mode 100644
index 0000000..db94424
--- /dev/null
+++ b/pkg/provider/f5apm/f5apm_test.go
@@ -0,0 +1,86 @@
+package f5apm
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
+	"net/http/cookiejar"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/versent/saml2aws/pkg/creds"
+
+	"github.com/versent/saml2aws/pkg/provider"
+
+	"github.com/stretchr/testify/require"
+)
+
+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()
+
+	jar, err := cookiejar.New(nil)
+	require.Nil(t, err)
+	ac := Client{client: &provider.HTTPClient{Client: http.Client{Jar: jar}}}
+	t.Log(ac)
+	loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "groundcontrol", Password: "majortom"}
+	t.Log(loginDetails)
+
+	authForm, err := ac.getLoginForm(loginDetails)
+	require.Nil(t, err)
+	require.Equal(t, url.Values{
+		"username": []string{"groundcontrol"},
+		"password": []string{"majortom"},
+		"vhost":    []string{"standard"},
+	}, authForm)
+}
+func TestClient_postLoginForm_user_pass(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()
+
+	jar, err := cookiejar.New(nil)
+	require.Nil(t, err)
+	ac := Client{client: &provider.HTTPClient{Client: http.Client{Jar: jar}}}
+	t.Log(ac)
+	loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "groundcontrol", Password: "majortom"}
+	t.Log(loginDetails)
+
+	authForm := url.Values{}
+	authForm.Add("username", "groundcontrol")
+	authForm.Add("password", "majortom")
+	resData, err := ac.postLoginForm(loginDetails, authForm)
+	require.Nil(t, err)
+	require.Equal(t, data, resData)
+}
+
+func TestClient_containsMFAForm(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)
+	mfaFound, mfaMethods := containsMFAForm(doc)
+	require.True(t, mfaFound)
+	require.Equal(t, []string{"push", "token"}, mfaMethods)
+}
+
+func TestClient_containsMFAForm_False(t *testing.T) {
+	data, err := ioutil.ReadFile("example/loginpage.html")
+	require.Nil(t, err)
+	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data))
+	require.Nil(t, err)
+	mfaFound, mfaMethods := containsMFAForm(doc)
+	require.False(t, mfaFound)
+	require.Equal(t, []string(nil), mfaMethods)
+}
diff --git a/saml2aws_test.go b/saml2aws_test.go
index 8ccb59f..2fc369d 100644
--- a/saml2aws_test.go
+++ b/saml2aws_test.go
@@ -10,7 +10,7 @@ func TestProviderList_Keys(t *testing.T) {
 
 	names := MFAsByProvider.Names()
 
-	require.Len(t, names, 11)
+	require.Len(t, names, 12)
 
 }