in pkg/provider/onelogin/onelogin.go [204:366]
func verifyMFA(oc *Client, oauthToken, appID, resp string) (string, error) {
stateToken := gjson.Get(resp, "state_token").String()
// choose an mfa option if there are multiple enabled
var option int
var mfaOptions []string
var preselected bool
for n, id := range gjson.Get(resp, "devices.#.device_type").Array() {
identifier := id.String()
if val, ok := supportedMfaOptions[identifier]; ok {
mfaOptions = append(mfaOptions, val)
// If there is pre-selected MFA option (thorugh the --mfa flag), then set MFA option index and break early.
if val == oc.MFA {
option = n
preselected = true
break
}
} else {
mfaOptions = append(mfaOptions, "UNSUPPORTED: "+identifier)
}
}
if !preselected && len(mfaOptions) > 1 {
option = prompter.Choose("Select which MFA option to use", mfaOptions)
}
factorID := gjson.Get(resp, fmt.Sprintf("devices.%d.device_id", option)).String()
callbackURL := gjson.Get(resp, "callback_url").String()
mfaIdentifer := gjson.Get(resp, fmt.Sprintf("devices.%d.device_type", option)).String()
mfaDeviceID := gjson.Get(resp, fmt.Sprintf("devices.%d.device_id", option)).String()
logger.WithField("factorID", factorID).WithField("callbackURL", callbackURL).WithField("mfaIdentifer", mfaIdentifer).Debug("MFA")
if _, ok := supportedMfaOptions[mfaIdentifer]; !ok {
return "", errors.New("unsupported mfa provider")
}
switch mfaIdentifer {
// These MFA options doesn't need additional request (e.g. to send SMS or a push notification etc) since the user can generate the code using their MFA app of choice.
case IdentifierTotpMfa, IdentifierYubiKey:
break
default:
var verifyBody bytes.Buffer
err := json.NewEncoder(&verifyBody).Encode(VerifyRequest{AppID: appID, DeviceID: mfaDeviceID, StateToken: stateToken})
if err != nil {
return "", errors.Wrap(err, "error encoding verifyReq")
}
req, err := http.NewRequest("POST", callbackURL, &verifyBody)
if err != nil {
return "", errors.Wrap(err, "error building verify request")
}
addContentHeaders(req)
addAuthHeader(req, oauthToken)
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)
if gjson.Get(resp, "status.error").Bool() {
msg := gjson.Get(resp, "status.message").String()
return "", errors.New(msg)
}
}
switch mfaIdentifer {
case IdentifierSmsMfa, IdentifierTotpMfa, IdentifierYubiKey:
verifyCode := prompter.StringRequired("Enter verification code")
var verifyBody bytes.Buffer
json.NewEncoder(&verifyBody).Encode(VerifyRequest{AppID: appID, DeviceID: mfaDeviceID, StateToken: stateToken, OTPToken: verifyCode})
req, err := http.NewRequest("POST", callbackURL, &verifyBody)
if err != nil {
return "", errors.Wrap(err, "error building token post request")
}
addContentHeaders(req)
addAuthHeader(req, oauthToken)
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)
message := gjson.Get(resp, "message").String()
if res.StatusCode != 200 || message != MessageSuccess {
return "", fmt.Errorf("HTTP %v: %s", res.StatusCode, message)
}
return gjson.Get(resp, "data").String(), nil
case IdentifierOneLoginProtectMfa:
// set the body payload to disable further push notifications (i.e. set do_not_notify to true)
// https://developers.onelogin.com/api-docs/2/saml-assertions/verify-factor
var verifyBody bytes.Buffer
err := json.NewEncoder(&verifyBody).Encode(VerifyRequest{AppID: appID, DeviceID: mfaDeviceID, DoNotNotify: true, StateToken: stateToken})
if err != nil {
return "", errors.New("error encoding verify MFA request body")
}
req, err := http.NewRequest("POST", callbackURL, &verifyBody)
if err != nil {
return "", errors.Wrap(err, "error building token post request")
}
addContentHeaders(req)
addAuthHeader(req, oauthToken)
fmt.Printf("\nWaiting for approval, please check your OneLogin Protect app ...")
started := time.Now()
// loop until success, error, or timeout
for {
if time.Since(started) > time.Minute {
log.Println(" Timeout")
return "", errors.New("User did not accept MFA in time")
}
logger.Debug("Verifying with OneLogin Protect")
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")
}
message := gjson.Get(string(body), "message").String()
// on 'error' status
if res.StatusCode != 200 {
return "", fmt.Errorf("HTTP %v: %s", res.StatusCode, message)
}
switch true {
case strings.Contains(message, "Authentication pending"):
time.Sleep(time.Second)
fmt.Print(".")
case message == MessageSuccess:
log.Println(" Approved")
return gjson.Get(string(body), "data").String(), nil
default:
log.Println(" Error:")
return "", errors.New("unsupported response from OneLogin, please raise ticket with saml2alibabacloud")
}
}
}
// catch all
return "", errors.New("no mfa options provided")
}