in pkg/provider/okta/okta.go [287:760]
func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, resp string) (string, error) {
stateToken := gjson.Get(resp, "stateToken").String()
// choose an mfa option if there are multiple enabled
mfaOption := 0
var mfaOptions []string
for i := range gjson.Get(resp, "_embedded.factors").Array() {
identifier := parseMfaIdentifer(resp, i)
if val, ok := supportedMfaOptions[identifier]; ok {
mfaOptions = append(mfaOptions, val)
} else {
mfaOptions = append(mfaOptions, "UNSUPPORTED: "+identifier)
}
}
if strings.ToUpper(oc.mfa) != "AUTO" {
for idx, val := range mfaOptions {
if strings.HasPrefix(strings.ToUpper(val), oc.mfa) {
mfaOption = idx
break
}
}
} else if len(mfaOptions) > 1 {
mfaOption = prompter.Choose("Select which MFA option to use", mfaOptions)
}
factorID := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d.id", mfaOption)).String()
oktaVerify := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d._links.verify.href", mfaOption)).String()
mfaIdentifer := parseMfaIdentifer(resp, mfaOption)
logger.WithField("factorID", factorID).WithField("oktaVerify", oktaVerify).WithField("mfaIdentifer", mfaIdentifer).Debug("MFA")
if _, ok := supportedMfaOptions[mfaIdentifer]; !ok {
return "", errors.New("unsupported mfa provider")
}
// get signature & callback
verifyReq := VerifyRequest{StateToken: stateToken}
verifyBody := new(bytes.Buffer)
// Login flow is different for YubiKeys ( of course )
// https://developer.okta.com/docs/reference/api/factors/#request-example-for-verify-yubikey-factor
// verifyBody needs to be a json document with the OTP from the yubikey in it.
// yay
switch mfa := mfaIdentifer; mfa {
case IdentifierYubiMfa:
verifyCode := prompter.Password("Press the button on your yubikey")
verifyReq.PassCode = verifyCode
}
err := json.NewEncoder(verifyBody).Encode(verifyReq)
if err != nil {
return "", errors.Wrap(err, "error encoding verifyReq")
}
req, err := http.NewRequest("POST", oktaVerify, verifyBody)
if err != nil {
return "", errors.Wrap(err, "error building verify request")
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
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)
switch mfa := mfaIdentifer; mfa {
case IdentifierYubiMfa:
return gjson.Get(resp, "sessionToken").String(), nil
case IdentifierSmsMfa, IdentifierTotpMfa, IdentifierOktaTotpMfa, IdentifierSymantecTotpMfa:
var verifyCode = loginDetails.MFAToken
if verifyCode == "" {
verifyCode = prompter.StringRequired("Enter verification code")
}
tokenReq := VerifyRequest{StateToken: stateToken, PassCode: verifyCode}
tokenBody := new(bytes.Buffer)
err = json.NewEncoder(tokenBody).Encode(tokenReq)
if err != nil {
return "", errors.Wrap(err, "error encoding token data")
}
req, err = http.NewRequest("POST", oktaVerify, tokenBody)
if err != nil {
return "", errors.Wrap(err, "error building token post request")
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
res, err := oc.client.Do(req)
if err != nil {
return "", errors.Wrap(err, "error retrieving token post response")
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", errors.Wrap(err, "error retrieving body from response")
}
resp = string(body)
return gjson.Get(resp, "sessionToken").String(), nil
case IdentifierPushMfa:
fmt.Printf("\nWaiting for approval, please check your Okta Verify app ...")
// loop until success, error, or timeout
for {
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")
}
// on 'success' status
if gjson.Get(string(body), "status").String() == "SUCCESS" {
fmt.Printf(" Approved\n\n")
return gjson.Get(string(body), "sessionToken").String(), nil
}
// otherwise probably still waiting
switch gjson.Get(string(body), "factorResult").String() {
case "WAITING":
time.Sleep(3 * time.Second)
fmt.Printf(".")
logger.Debug("Waiting for user to authorize login")
case "TIMEOUT":
fmt.Printf(" Timeout\n")
return "", errors.New("User did not accept MFA in time")
case "REJECTED":
fmt.Printf(" Rejected\n")
return "", errors.New("MFA rejected by user")
default:
fmt.Printf(" Error\n")
return "", errors.New("Unsupported response from Okta, please raise ticket with saml2alibabacloud")
}
}
case IdentifierDuoMfa:
duoHost := gjson.Get(resp, "_embedded.factor._embedded.verification.host").String()
duoSignature := gjson.Get(resp, "_embedded.factor._embedded.verification.signature").String()
duoSiguatres := strings.Split(duoSignature, ":")
//duoSignatures[0] = TX
//duoSignatures[1] = APP
duoCallback := gjson.Get(resp, "_embedded.factor._embedded.verification._links.complete.href").String()
// 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/signin/verify/duo/web", oktaOrgHost))
duoForm.Add("java_version", "")
duoForm.Add("java_version", "")
duoForm.Add("flash_version", "")
duoForm.Add("screen_resolution_width", "3008")
duoForm.Add("screen_resolution_height", "1692")
duoForm.Add("color_depth", "24")
req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode()))
if err != nil {
return "", errors.Wrap(err, "error building authentication request")
}
q := req.URL.Query()
q.Add("tx", duoSiguatres[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 retrieving verify response")
}
//try to extract sid
doc, err := goquery.NewDocumentFromResponse(res)
if err != nil {
return "", errors.Wrap(err, "error parsing document")
}
duoSID, ok := doc.Find("input[name=\"sid\"]").Attr("value")
if !ok {
return "", errors.Wrap(err, "unable to locate saml 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 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)
duoTxStat := gjson.Get(resp, "stat").String()
duoTxID := gjson.Get(resp, "response.txid").String()
if duoTxStat != "OK" {
return "", errors.New("error authenticating 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 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()
newSID := gjson.Get(resp, "response.sid").String()
if newSID != "" {
duoSID = newSID
}
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()
newSID = gjson.Get(resp, "response.sid").String()
if newSID != "" {
duoSID = newSID
}
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)
duoForm = url.Values{}
duoForm.Add("sid", duoSID)
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)
duoTxStat = gjson.Get(resp, "stat").String()
if duoTxStat != "OK" {
message := gjson.Get(resp, "message").String()
return "", fmt.Errorf("duoResultSubmit: %s %s", duoTxStat, message)
}
duoTxCookie := gjson.Get(resp, "response.cookie").String()
if duoTxCookie == "" {
return "", errors.New("duoResultSubmit: Unable to get response.cookie")
}
// callback to okta with cookie
oktaForm := url.Values{}
oktaForm.Add("id", factorID)
oktaForm.Add("stateToken", stateToken)
oktaForm.Add("sig_response", fmt.Sprintf("%s:%s", duoTxCookie, duoSiguatres[1]))
req, err = http.NewRequest("POST", duoCallback, strings.NewReader(oktaForm.Encode()))
if err != nil {
return "", errors.Wrap(err, "error building authentication request")
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
_, err = oc.client.Do(req) // TODO: check result
if err != nil {
return "", errors.Wrap(err, "error retrieving verify response")
}
// extract okta session token
verifyReq = VerifyRequest{StateToken: stateToken}
verifyBody = new(bytes.Buffer)
err = json.NewEncoder(verifyBody).Encode(verifyReq)
if err != nil {
return "", errors.Wrap(err, "error encoding verify request")
}
req, err = http.NewRequest("POST", oktaVerify, verifyBody)
if err != nil {
return "", errors.Wrap(err, "error building verify request")
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("X-Okta-XsrfToken", "")
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")
}
return gjson.GetBytes(body, "sessionToken").String(), nil
case IdentifierFIDOWebAuthn:
nonce := gjson.Get(resp, "_embedded.factor._embedded.challenge.challenge").String()
credentialID := gjson.Get(resp, "_embedded.factor.profile.credentialId").String()
version := gjson.Get(resp, "_embedded.factor.profile.version").String()
appID := oktaOrgHost
webauthnCallback := gjson.Get(resp, "_links.next.href").String()
fidoClient, err := NewFidoClient(nonce,
appID,
version,
credentialID,
stateToken,
new(U2FDeviceFinder))
if err != nil {
return "", err
}
signedAssertion, err := fidoClient.ChallengeU2F()
if err != nil {
return "", err
}
payload, err := json.Marshal(signedAssertion)
if err != nil {
return "", err
}
req, err = http.NewRequest("POST", webauthnCallback, strings.NewReader(string(payload)))
if err != nil {
return "", errors.Wrap(err, "error building authentication request")
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")
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")
}
return gjson.GetBytes(body, "sessionToken").String(), nil
}
// catch all
return "", errors.New("no mfa options provided")
}