Repository: Versent/saml2aws
Pull request #355 (2019-10-04 18:47:34)

Added Akamai EAA IdP support for saml2aws
Adding Akamai EAA IdP provider support. It supports various MFA features like "DUO", "SMS", "EMAIL", "TOTP"


Patch:
diff --git a/README.md b/README.md
index e86185e..5a69c49 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ The process goes something like this:
   * [Shibboleth](pkg/provider/shibboleth/README.md)
   * [F5APM](pkg/provider/f5apm/README.md)
   * [PSU](pkg/provider/psu/README.md)
+  * [Akamai](pkg/provider/akamai/README.md)
   * OneLogin
 * AWS SAML Provider configured
 
diff --git a/pkg/provider/akamai/README.md b/pkg/provider/akamai/README.md
new file mode 100644
index 0000000..c059fcd
--- /dev/null
+++ b/pkg/provider/akamai/README.md
@@ -0,0 +1,46 @@
+# EAA Akamai IDP support for Saml2AWS
+This code supports to authenticate user from cli with saml to Akamai SAML IDP without browser
+
+# Requirements
+* Need Go 1.12
+* Need saml2aws code from github link https://github.com/Versent/saml2aws
+
+# Building the Code
+* Install Go 1.12
+* Set GOPATH
+* clone code from github link to $GOPATH/src/github.com/versent/saml2aws
+* copy akamai.go to versent/saml2aws/pkg/providers/akamai/
+* Merge code from saml2aws.go to support Akamai config.
+* Ensure $GOPATH/bin in your $PATH
+* make deps
+* make install
+* Binary will be present in GOPATH/bin
+
+# Configuring the SAML IDP
+* create Akamai EAA IDP
+* Create a saml saas app
+* Add Attribute as mentioned below in example.
+
+"attrmap": [
+     {
+          "fmt": "uri_reference",
+          "name": "https://aws.amazon.com/SAML/Attributes/Role",
+          "src": "",
+          "val": "arn:aws:iam::432929478872:saml-provider/AkamaiIDP,arn:aws:iam::432929478872:role/AkamaiIDProle"
+     },
+     {
+          "fmt": "basic",
+          "name": "https://aws.amazon.com/SAML/Attributes/RoleSessionName",
+          "val": "punit@qadomain.com"
+     },
+     {
+          "fmt": "basic",
+          "name": "https://aws.amazon.com/SAML/Attributes/SessionDuration",
+          "val": "1200"
+     }
+]
+
+# Using the saml2aws
+* Configure IDP account run command -  saml2aws configure.
+* Add url as https://<EAAIDP>/?app=<SAAShostname> Eg: https://samlidp.example.com/?app=signing.aws.amazon.com
+* To login using saml2aws run command - saml2aws login
diff --git a/pkg/provider/akamai/akamai.go b/pkg/provider/akamai/akamai.go
new file mode 100644
index 0000000..0b63df0
--- /dev/null
+++ b/pkg/provider/akamai/akamai.go
@@ -0,0 +1,673 @@
+package akamai
+
+import (
+	"bytes"
+	"fmt"
+	"html"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	"github.com/versent/saml2aws/pkg/prompter"
+
+	"github.com/PuerkitoBio/goquery"
+	"github.com/pkg/errors"
+	"github.com/tidwall/gjson"
+	"github.com/versent/saml2aws/pkg/cfg"
+	"github.com/versent/saml2aws/pkg/creds"
+	"github.com/versent/saml2aws/pkg/provider"
+
+	"encoding/json"
+)
+
+const (
+	IdentifierDuoMfa   = "duo"
+	IdentifierSmsMfa   = "sms"
+	IdentifierEmailMfa = "email"
+	IdentifierTotpMfa  = "totp"
+)
+
+var logger = logrus.WithField("provider", "akamai")
+
+var (
+	supportedMfaOptions = map[string]MfaUserOption{
+		IdentifierDuoMfa:   {"DUO MFA authentication", "duo"},
+		IdentifierSmsMfa:   {"SMS MFA authentication", "sms"},
+		IdentifierEmailMfa: {"EMAIL MFA authentication", "email"},
+		IdentifierTotpMfa:  {"TOTP MFA authentication", "totp"},
+	}
+)
+
+type MfaUserOption struct {
+	UserDisplayString string
+	UserMfaOption     string
+}
+
+// Client is a wrapper representing a Akamai SAML client
+type Client struct {
+	client *provider.HTTPClient
+	mfa    string
+}
+
+// AuthRequest represents an mfa Akamai request
+type AuthRequest struct {
+	Username string `json:"username"`
+	Password string `json:"password"`
+}
+
+// Navigate request for saml
+type NavRequest struct {
+	Hostname string `json:"hostname"`
+}
+
+type MfaPushRequest struct {
+	Force bool   `json:"force"`
+	Uuid  string `json:"uuid"`
+}
+
+type MfaTokenVerify struct {
+	Category       string `json:"category"`
+	Token          string `json:"token"`
+	Uuid           string `json:"uuid"`
+	DuoSigRequest  string `json:"sig_request"`
+	DuoSigResponse string `json:"sig_response"`
+}
+
+// New creates a new Akamai 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")
+	}
+
+	// assign a response validator to ensure all responses are either success or a redirect
+	// this is to avoid have explicit checks for every single response
+	client.CheckResponseStatus = provider.SuccessOrRedirectResponseValidator
+
+	return &Client{
+		client: client,
+		mfa:    idpAccount.MFA,
+	}, nil
+}
+
+// Authenticate logs into Akamai and returns a SAML response
+func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
+
+	var samlAssertion string
+
+	akamaiURL, err := url.Parse(loginDetails.URL)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error building akamaiURL")
+	}
+
+	akamaiOrgHost := akamaiURL.Host
+
+	akamaiQuery := akamaiURL.Query()
+	akamaiSamlApp := string(akamaiQuery.Get("app"))
+
+	// Get xsrf data and cookie by doing get request
+	akamaiLoginURL := fmt.Sprintf("https://%s/", akamaiOrgHost)
+	req, err := http.NewRequest("GET", akamaiLoginURL, nil)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error building authentication request")
+	}
+
+	res, err := oc.client.Do(req)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error retrieving login request")
+	}
+
+	doc, err := goquery.NewDocumentFromResponse(res)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error parsing document")
+	}
+
+	xsrfToken, ok := doc.Find("input[id=\"xsrf\"]").Attr("value")
+	if !ok {
+		return samlAssertion, errors.Wrap(err, "unable to locate xsrf token in html")
+	}
+
+	// Send login request to Akamai
+	authReq := AuthRequest{Username: loginDetails.Username, Password: loginDetails.Password}
+	authBody := new(bytes.Buffer)
+	err = json.NewEncoder(authBody).Encode(authReq)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error encoding authreq")
+	}
+	authSubmitURL := fmt.Sprintf("https://%s/api/v1/login", akamaiOrgHost)
+
+	loginReq, err := http.NewRequest("POST", authSubmitURL, authBody)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error building authentication request")
+	}
+
+	loginReq.Header.Add("Content-Type", "application/json")
+	loginReq.Header.Add("Accept", "application/json")
+	loginReq.Header.Add("xsrf", string(xsrfToken))
+
+	res, err = oc.client.Do(loginReq)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error login to EAA IDP")
+	}
+
+	body, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error retrieving body from response")
+	}
+
+	resp := string(body)
+	authStatus := gjson.Get(resp, "status").String()
+	if authStatus != "200" {
+		authFailReason := gjson.Get(resp, "msg").String()
+		fmt.Printf("Login Failed %s\n", authFailReason)
+		logger.Debug("Login Failed:", authFailReason)
+		return samlAssertion, errors.Wrap(err, "Login Failure")
+	}
+
+	// Send saml navigate request to Akamai
+	navReq := NavRequest{Hostname: akamaiSamlApp}
+	navBody := new(bytes.Buffer)
+	err = json.NewEncoder(navBody).Encode(navReq)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error encoding navreq")
+	}
+	navSubmitURL := fmt.Sprintf("https://%s/api/v2/apps/navigate", akamaiOrgHost)
+
+	navloginReq, err := http.NewRequest("POST", navSubmitURL, navBody)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error building navigation request")
+	}
+	navloginReq.Header.Add("Content-Type", "application/json")
+	navloginReq.Header.Add("Accept", "application/json")
+	navloginReq.Header.Add("xsrf", string(xsrfToken))
+
+	res, err = oc.client.Do(navloginReq)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error while navigation request to EAA ")
+	}
+
+	body, err = ioutil.ReadAll(res.Body)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error retrieving response from navigate request")
+	}
+
+	mfaStatus := gjson.GetBytes(body, "mfa.status").String()
+	if mfaStatus == "verify" {
+		err = verifyMfa(oc, akamaiOrgHost, loginDetails, xsrfToken)
+		if err != nil {
+			return samlAssertion, errors.Wrap(err, "error verifying MFA")
+		}
+	} else if mfaStatus == "register" {
+		fmt.Printf("MFA is enabled but not registered for user. Register MFA by accessing EAA IDP from Browser\n")
+		logger.Debug("MFA is enabled but not registered for user")
+		return samlAssertion, errors.Wrap(err, "register mfa by logging to IDP")
+	}
+
+	/* MFA is done call navigate again */
+	navReq = NavRequest{Hostname: akamaiSamlApp}
+	navBody = new(bytes.Buffer)
+	err = json.NewEncoder(navBody).Encode(navReq)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error encoding authreq")
+	}
+	navSubmitURL = fmt.Sprintf("https://%s/api/v2/apps/navigate", akamaiOrgHost)
+
+	navloginReq, err = http.NewRequest("POST", navSubmitURL, navBody)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error sending final navigate request")
+	}
+	navloginReq.Header.Add("Content-Type", "application/json")
+	navloginReq.Header.Add("Accept", "application/json")
+	navloginReq.Header.Add("xsrf", string(xsrfToken))
+
+	res, err = oc.client.Do(navloginReq)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error navigate request to EAA ")
+	}
+
+	body, err = ioutil.ReadAll(res.Body)
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error retrieving body from response")
+	}
+
+	/* saml assertion response from json of navigate */
+	samlResponseHtml := gjson.GetBytes(body, "navigate.body").String()
+	doc, err = goquery.NewDocumentFromReader(strings.NewReader(samlResponseHtml))
+	if err != nil {
+		return samlAssertion, errors.Wrap(err, "error parsing saml response in document")
+	}
+
+	samlAssertion, ok = doc.Find("input[name=\"SAMLResponse\"]").Attr("value")
+	if !ok {
+		return samlAssertion, errors.Wrap(err, "unable to locate SAMLResponse in html")
+	}
+
+	logger.Debug("auth complete")
+	return samlAssertion, nil
+}
+
+func verifyMfa(oc *Client, akamaiOrgHost string, loginDetails *creds.LoginDetails, xsrfToken string) error {
+
+	/* Get supported MFA for this login */
+	mfaConfigURL := fmt.Sprintf("https://%s/api/v1/config/mfa", akamaiOrgHost)
+	mfaConfigReq, err := http.NewRequest("GET", mfaConfigURL, nil)
+	if err != nil {
+		return errors.Wrap(err, "error building mfa config request")
+	}
+	mfaConfigReq.Header.Add("Accept", "application/json")
+	mfaConfigReq.Header.Add("xsrf", string(xsrfToken))
+	res, err := oc.client.Do(mfaConfigReq)
+	if err != nil {
+		return errors.Wrap(err, "error mfa config request to EAA ")
+	}
+	body, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return errors.Wrap(err, "error retrieving mfa config request")
+	}
+
+	mfaConfigData := gjson.GetBytes(body, "mfa.config.options")
+	if mfaConfigData.Index == 0 {
+		fmt.Println("Mfa Config option not found")
+		return errors.Wrap(err, "Mfa not configured ")
+	}
+
+	/* Mfa config data present or not check otherwise return directly */
+	mfaOption := 0
+	var mfaOptions []MfaUserOption
+	for _, name := range mfaConfigData.Array() {
+		identifier := name.String()
+		if val, ok := supportedMfaOptions[identifier]; ok {
+			mfaOptions = append(mfaOptions, val)
+		}
+	}
+
+	/* Get MFA token settings from IDP */
+	mfaSettingURL := fmt.Sprintf("https://%s/api/v1/mfa/token/settings", akamaiOrgHost)
+	mfaSettingReq, err := http.NewRequest("GET", mfaSettingURL, nil)
+	if err != nil {
+		return errors.Wrap(err, "error building mfa setting request")
+	}
+	mfaSettingReq.Header.Add("Accept", "application/json")
+	mfaSettingReq.Header.Add("xsrf", string(xsrfToken))
+	res, err = oc.client.Do(mfaSettingReq)
+	if err != nil {
+		return errors.Wrap(err, "error mfa setting request to EAA ")
+	}
+	mfaSettingData, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return errors.Wrap(err, "error retrieving body from response")
+	}
+
+	var mfaDisplayOptions []string
+	var mfaUserOption string
+	var mfaConfiguredSupported int
+
+	if oc.mfa != "Auto" {
+		mfaUserOption = strings.ToLower(oc.mfa)
+		for _, val := range mfaOptions {
+			if mfaUserOption == val.UserMfaOption {
+				mfaDisplayOptions = nil
+				mfaConfiguredSupported = 1
+				break
+			}
+			mfaDisplayOptions = append(mfaDisplayOptions, val.UserDisplayString)
+		}
+
+		mfaDisplayNum := len(mfaDisplayOptions)
+		if mfaDisplayNum > 1 {
+			mfaOption = prompter.Choose("Select which MFA option to use", mfaDisplayOptions)
+			mfaUserOption = mfaOptions[mfaOption].UserMfaOption
+		} else if mfaDisplayNum == 1 {
+			mfaUserOption = mfaOptions[1].UserMfaOption
+		} else if mfaDisplayNum == 0 && mfaConfiguredSupported != 1 {
+			return errors.New("unsupported mfa provider")
+		}
+	} else {
+		mfaUserOption = gjson.GetBytes(mfaSettingData, "mfa.settings.preferred.option").String()
+	}
+
+	if _, ok := supportedMfaOptions[mfaUserOption]; !ok {
+		return errors.New("unsupported mfa provider")
+	}
+
+	/* specific mfa */
+	switch mfa := mfaUserOption; mfa {
+
+	case IdentifierSmsMfa, IdentifierEmailMfa, IdentifierTotpMfa:
+
+		/* 1. Get MFA UUID */
+		mfaUuid := fmt.Sprintf("mfa.settings.%s.0.uuid", mfa)
+		uuidMfa := gjson.GetBytes(mfaSettingData, mfaUuid).String()
+
+		/* 2.Push MFA */
+		var mfaApi = mfa
+		if mfa == IdentifierSmsMfa {
+			mfaApi = "phone"
+		}
+
+		var mfaResStatus string
+		if mfa == IdentifierSmsMfa || mfa == IdentifierEmailMfa {
+			mfaPushURL := fmt.Sprintf("https://%s/api/v1/mfa/user/%s/token/push", akamaiOrgHost, mfaApi)
+			mfaPushData := MfaPushRequest{Force: false, Uuid: uuidMfa}
+			mfaPushBody := new(bytes.Buffer)
+			err = json.NewEncoder(mfaPushBody).Encode(mfaPushData)
+			if err != nil {
+				return errors.Wrap(err, "error encoding mfa push data ")
+			}
+
+			mfaPushReq, err := http.NewRequest("POST", mfaPushURL, mfaPushBody)
+			if err != nil {
+				return errors.Wrap(err, "error building mfa push request")
+			}
+
+			mfaPushReq.Header.Add("Content-Type", "application/json")
+			mfaPushReq.Header.Add("Accept", "application/json")
+			mfaPushReq.Header.Add("xsrf", string(xsrfToken))
+
+			res, err = oc.client.Do(mfaPushReq)
+			if err != nil {
+				return errors.Wrap(err, "error while sending MFA push code ")
+			}
+
+			body, err = ioutil.ReadAll(res.Body)
+			if err != nil {
+				return errors.Wrap(err, "error retrieving MFA push response ")
+			}
+
+			mfaResStatus := gjson.GetBytes(body, "status").String()
+
+			if mfaResStatus != "200" {
+				return errors.Wrap(err, "Unable to send mfa token")
+			}
+
+		}
+		/* 3. Verify MFA */
+
+		verifyCode := prompter.StringRequired("Enter MFA verification code")
+
+		mfaVerifyURL := fmt.Sprintf("https://%s/api/v1/mfa/user/%s/token/verify", akamaiOrgHost, mfaApi)
+		mfaVerifyData := MfaTokenVerify{Category: mfa, Token: verifyCode, Uuid: uuidMfa}
+		mfaVerifyBody := new(bytes.Buffer)
+		err = json.NewEncoder(mfaVerifyBody).Encode(mfaVerifyData)
+		if err != nil {
+			return errors.Wrap(err, "error encoding mfa verify req")
+		}
+		mfaVerifyReq, err := http.NewRequest("POST", mfaVerifyURL, mfaVerifyBody)
+		if err != nil {
+			return errors.Wrap(err, "error creating mfa verification request")
+		}
+
+		mfaVerifyReq.Header.Add("Content-Type", "application/json")
+		mfaVerifyReq.Header.Add("Accept", "application/json")
+		mfaVerifyReq.Header.Add("xsrf", string(xsrfToken))
+
+		res, err = oc.client.Do(mfaVerifyReq)
+		if err != nil {
+			return errors.Wrap(err, "error verifying mfa to EAA ")
+		}
+
+		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return errors.Wrap(err, "error retrieving mfa verify response")
+		}
+
+		mfaResStatus = gjson.GetBytes(body, "status").String()
+		if mfaResStatus != "200" {
+			return errors.Wrap(err, "Unable to verify mfa token")
+		}
+
+		return nil
+
+	case IdentifierDuoMfa:
+
+		duoSettings := fmt.Sprintf("mfa.settings.%s.0", mfa)
+
+		duoHost := gjson.GetBytes(mfaSettingData, duoSettings).Get("duo_host").String()
+		duoSignature := gjson.GetBytes(mfaSettingData, duoSettings).Get("token").String()
+		duoSignatures := strings.Split(duoSignature, ":")
+
+		//duoSignatures[0] = TX
+		//duoSignatures[1] = APP
+
+		// initiate duo mfa to get sid
+		duoSubmitURL := fmt.Sprintf("https://%s/frame/web/v1/auth", duoHost)
+
+		duoForm := url.Values{}
+		duoForm.Add("parent", fmt.Sprintf("https://%s/#/token", akamaiOrgHost))
+		duoForm.Add("java_version", "")
+		duoForm.Add("java_version", "")
+		duoForm.Add("flash_version", "")
+		duoForm.Add("screen_resolution_width", "1440")
+		duoForm.Add("screen_resolution_height", "900")
+		duoForm.Add("color_depth", "24")
+
+		req, err := http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode()))
+		if err != nil {
+			return errors.Wrap(err, "error building duo request")
+		}
+		q := req.URL.Query()
+		q.Add("tx", duoSignatures[0])
+		req.URL.RawQuery = q.Encode()
+
+		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+		res, err = oc.client.Do(req)
+		if err != nil {
+			return errors.Wrap(err, "error sending duo request")
+		}
+
+		//try to extract sid
+		doc, err := goquery.NewDocumentFromResponse(res)
+		if err != nil {
+			return errors.Wrap(err, "error parsing document from duo")
+		}
+
+		duoSID, ok := doc.Find("input[name=\"sid\"]").Attr("value")
+		if !ok {
+			return errors.Wrap(err, "unable to locate sid in duo response")
+		}
+		duoSID = html.UnescapeString(duoSID)
+
+		//prompt for mfa type
+		//only supporting push or passcode for now
+		var token string
+
+		var duoMfaOptions = []string{
+			"Duo Push",
+			"Passcode",
+		}
+
+		duoMfaOption := 0
+
+		if loginDetails.DuoMFAOption == "Duo Push" {
+			duoMfaOption = 0
+		} else if loginDetails.DuoMFAOption == "Passcode" {
+			duoMfaOption = 1
+		} else {
+			duoMfaOption = prompter.Choose("Select a DUO MFA Option", duoMfaOptions)
+		}
+
+		if duoMfaOptions[duoMfaOption] == "Passcode" {
+			//get users DUO MFA Token
+			token = prompter.StringRequired("Enter passcode")
+		}
+
+		// send mfa auth request
+		duoSubmitURL = fmt.Sprintf("https://%s/frame/prompt", duoHost)
+
+		duoForm = url.Values{}
+		duoForm.Add("sid", duoSID)
+		duoForm.Add("device", "phone1")
+		duoForm.Add("factor", duoMfaOptions[duoMfaOption])
+		duoForm.Add("out_of_date", "false")
+		if duoMfaOptions[duoMfaOption] == "Passcode" {
+			duoForm.Add("passcode", token)
+		}
+
+		req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode()))
+		if err != nil {
+			return errors.Wrap(err, "error building duo prompt request")
+		}
+
+		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+		res, err = oc.client.Do(req)
+		if err != nil {
+			return errors.Wrap(err, "error retrieving duo prompt request")
+		}
+
+		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return errors.Wrap(err, "error retrieving duo prompt response")
+		}
+
+		resp := string(body)
+
+		duoTxStat := gjson.Get(resp, "stat").String()
+		duoTxID := gjson.Get(resp, "response.txid").String()
+		if duoTxStat != "OK" {
+			return errors.Wrap(err, "error authenticating duo mfa device")
+		}
+
+		// get duo cookie
+		duoSubmitURL = fmt.Sprintf("https://%s/frame/status", duoHost)
+
+		duoForm = url.Values{}
+		duoForm.Add("sid", duoSID)
+		duoForm.Add("txid", duoTxID)
+
+		req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode()))
+		if err != nil {
+			return errors.Wrap(err, "error building duo status request")
+		}
+
+		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+		res, err = oc.client.Do(req)
+		if err != nil {
+			return errors.Wrap(err, "error sending duo status request")
+		}
+
+		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return errors.Wrap(err, "error retrieving duo status response")
+		}
+
+		resp = string(body)
+
+		duoTxResult := gjson.Get(resp, "response.result").String()
+		duoResultURL := gjson.Get(resp, "response.result_url").String()
+
+		fmt.Println(gjson.Get(resp, "response.status").String())
+
+		if duoTxResult != "SUCCESS" {
+			//poll as this is likely a push request
+			for {
+				time.Sleep(3 * time.Second)
+
+				req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode()))
+				if err != nil {
+					return errors.Wrap(err, "error building authentication request")
+				}
+
+				req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+				res, err = oc.client.Do(req)
+				if err != nil {
+					return errors.Wrap(err, "error retrieving verify response")
+				}
+
+				body, err = ioutil.ReadAll(res.Body)
+				if err != nil {
+					return errors.Wrap(err, "error retrieving body from response")
+				}
+
+				resp := string(body)
+
+				duoTxResult = gjson.Get(resp, "response.result").String()
+				duoResultURL = gjson.Get(resp, "response.result_url").String()
+
+				fmt.Println(gjson.Get(resp, "response.status").String())
+
+				if duoTxResult == "FAILURE" {
+					return errors.Wrap(err, "failed to authenticate device")
+				}
+
+				if duoTxResult == "SUCCESS" {
+					break
+				}
+			}
+		}
+
+		duoRequestURL := fmt.Sprintf("https://%s%s", duoHost, duoResultURL)
+		req, err = http.NewRequest("POST", duoRequestURL, strings.NewReader(duoForm.Encode()))
+		if err != nil {
+			return errors.Wrap(err, "error constructing request object to result url")
+		}
+
+		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+		res, err = oc.client.Do(req)
+		if err != nil {
+			return errors.Wrap(err, "error retrieving duo result response")
+		}
+
+		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return errors.Wrap(err, "duoResultSubmit: error retrieving body from response")
+		}
+
+		resp = string(body)
+		duoTxCookie := gjson.Get(resp, "response.cookie").String()
+		if duoTxCookie == "" {
+			return errors.Wrap(err, "duoResultSubmit: Unable to get response.cookie")
+		}
+
+		// callback to Akamai to verify
+
+		mfaVerifyURL := fmt.Sprintf("https://%s/api/v1/mfa/user/%s/token/verify", akamaiOrgHost, mfa)
+		mfaDuoSigResponse := fmt.Sprintf("%s:%s", duoTxCookie, duoSignatures[1])
+		mfaVerifyData := MfaTokenVerify{Category: mfa, Uuid: mfa,
+			DuoSigRequest: duoSignature, DuoSigResponse: mfaDuoSigResponse}
+		mfaVerifyBody := new(bytes.Buffer)
+		err = json.NewEncoder(mfaVerifyBody).Encode(mfaVerifyData)
+		if err != nil {
+			return errors.Wrap(err, "error encoding duo mfa verify req")
+		}
+		mfaVerifyReq, err := http.NewRequest("POST", mfaVerifyURL, mfaVerifyBody)
+		if err != nil {
+			return errors.Wrap(err, "error creating duo mfa verification request")
+		}
+
+		mfaVerifyReq.Header.Add("Content-Type", "application/json")
+		mfaVerifyReq.Header.Add("Accept", "application/json")
+		mfaVerifyReq.Header.Add("xsrf", string(xsrfToken))
+
+		res, err = oc.client.Do(mfaVerifyReq)
+		if err != nil {
+			return errors.Wrap(err, "error sending duo mfa request to EAA ")
+		}
+
+		body, err = ioutil.ReadAll(res.Body)
+		if err != nil {
+			return errors.Wrap(err, "error retrieving duo mfa response ")
+		}
+
+		mfaResStatus := gjson.GetBytes(body, "status").String()
+		if mfaResStatus != "200" {
+			return errors.Wrap(err, "Unable to verify mfa token")
+		}
+
+		return nil
+
+	}
+
+	return errors.New("no mfa options provided")
+
+}
diff --git a/saml2aws.go b/saml2aws.go
index 888ca8c..8631854 100644
--- a/saml2aws.go
+++ b/saml2aws.go
@@ -19,6 +19,7 @@ import (
 	"github.com/versent/saml2aws/pkg/provider/pingone"
 	"github.com/versent/saml2aws/pkg/provider/psu"
 	"github.com/versent/saml2aws/pkg/provider/shibboleth"
+	"github.com/versent/saml2aws/pkg/provider/akamai"
 )
 
 // ProviderList list of providers with their MFAs
@@ -39,6 +40,7 @@ var MFAsByProvider = ProviderList{
 	"Shibboleth": []string{"Auto"},
 	"PSU":        []string{"Auto"},
 	"F5APM":      []string{"Auto"},
+	"Akamai":     []string{"Auto", "DUO", "SMS", "EMAIL", "TOTP"},
 }
 
 // Names get a list of provider names
@@ -149,6 +151,11 @@ func NewSAMLClient(idpAccount *cfg.IDPAccount) (SAMLClient, error) {
 			return nil, fmt.Errorf("Invalid MFA type: %v for %v provider", idpAccount.MFA, idpAccount.Provider)
 		}
 		return f5apm.New(idpAccount)
+	case "Akamai":
+		if invalidMFA(idpAccount.Provider, idpAccount.MFA) {
+			return nil, fmt.Errorf("Invalid MFA type: %v for %v provider", idpAccount.MFA, idpAccount.Provider)
+		}
+		return akamai.New(idpAccount)
 
 	default:
 		return nil, fmt.Errorf("Invalid provider: %v", idpAccount.Provider)


Test patch:
diff --git a/saml2aws_test.go b/saml2aws_test.go
index 33700e7..23e52eb 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, 13)
+	require.Len(t, names, 14)
 
 }