func verifyMfa()

in pkg/provider/akamai/akamai.go [256:674]


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 {
		log.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()

		log.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()

				log.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")

}