diff --git a/pkg/provider/jumpcloud/jumpcloud.go b/pkg/provider/jumpcloud/jumpcloud.go index 5f1b9abbd..a53df4e23 100644 --- a/pkg/provider/jumpcloud/jumpcloud.go +++ b/pkg/provider/jumpcloud/jumpcloud.go @@ -23,22 +23,25 @@ import ( ) const ( - jcSSOBaseURL = "https://sso.jumpcloud.com/" - xsrfURL = "https://console.jumpcloud.com/userconsole/xsrf" - authSubmitURL = "https://console.jumpcloud.com/userconsole/auth" - webauthnSubmitURL = "https://console.jumpcloud.com/userconsole/auth/webauthn" - duoAuthSubmitURL = "https://console.jumpcloud.com/userconsole/auth/duo" - - IdentifierTotpMfa = "totp" - IdentifierDuoMfa = "duo" - IdentifierU2F = "webauthn" + jcSSOBaseURL = "https://sso.jumpcloud.com/" + xsrfURL = "https://console.jumpcloud.com/userconsole/xsrf" + authSubmitURL = "https://console.jumpcloud.com/userconsole/auth" + webauthnSubmitURL = "https://console.jumpcloud.com/userconsole/auth/webauthn" + duoAuthSubmitURL = "https://console.jumpcloud.com/userconsole/auth/duo" + jumpCloudProtectSubmitURL = "https://console.jumpcloud.com/userconsole/auth/push" + + IdentifierTotpMfa = "totp" + IdentifierDuoMfa = "duo" + IdentifierU2F = "webauthn" + IdentifierJumpCloudProtect = "push" ) var ( supportedMfaOptions = map[string]string{ - IdentifierTotpMfa: "TOTP MFA authentication", - IdentifierDuoMfa: "DUO MFA authentication", - IdentifierU2F: "FIDO WebAuthn authentication", + IdentifierTotpMfa: "TOTP MFA authentication", + IdentifierDuoMfa: "DUO MFA authentication", + IdentifierU2F: "FIDO WebAuthn authentication", + IdentifierJumpCloudProtect: "PUSH MFA authentication (JumpCloud Protect)", } ) @@ -310,6 +313,9 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") return jc.client.Do(req) + + case IdentifierJumpCloudProtect: + return jc.jumpCloudProtectAuth(xsrfToken) case IdentifierDuoMfa: // Get Duo config req, err := http.NewRequest("GET", duoAuthSubmitURL, nil) @@ -577,7 +583,6 @@ func (jc *Client) verifyMFA(jumpCloudOrgHost string, loginDetails *creds.LoginDe } func (jc *Client) getUserOption(body []byte) (string, error) { - // data =>map[factors:[map[status:available type:totp] map[status:available type:webauthn]] message:MFA required.] mfaConfigData := gjson.GetBytes(body, "factors") if mfaConfigData.Index == 0 { log.Fatalln("Mfa Config option not found") diff --git a/pkg/provider/jumpcloud/jumpcloud_protect.go b/pkg/provider/jumpcloud/jumpcloud_protect.go new file mode 100644 index 000000000..082069488 --- /dev/null +++ b/pkg/provider/jumpcloud/jumpcloud_protect.go @@ -0,0 +1,103 @@ +package jumpcloud + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "path" + "time" + + "github.com/pkg/errors" +) + +func (jc *Client) jumpCloudProtectAuth(xsrfToken string) (*http.Response, error) { + jumpCloudParsedURL, err := url.Parse(jumpCloudProtectSubmitURL) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unable to parse submit url, url=%s", jumpCloudProtectSubmitURL)) + } + + req, err := http.NewRequest("POST", jumpCloudParsedURL.String(), emptyJSONIOReader()) + if err != nil { + return nil, errors.Wrap(err, "error building jumpcloud protect auth request") + } + ensureHeaders(xsrfToken, req) + + res, err := jc.client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "error retrieving JumpCloud PUSH configuration") + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return nil, errors.New("error retrieving JumpCloud PUSH configuration, non 200 status returned") + } + + jpResp, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, errors.Wrap(err, "error retrieving Duo configuration") + } + + type JP struct { + ID string `json:"id"` + ExpiresAt time.Time `json:"expiresAt"` + InitiatedAt time.Time `json:"initiatedAt"` + Status string `json:"status"` + UserId string `json:"userId"` + } + + jp := JP{} + if err := json.Unmarshal(jpResp, &jp); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal JumpCloud Protect configuration") + } + + jumpCloudParsedURL.Path = path.Join(jumpCloudParsedURL.Path, jp.ID) + req, err = http.NewRequest("GET", jumpCloudParsedURL.String(), nil) + + if err != nil { + return nil, errors.Wrap(err, "failed to build JumpCoud push polling request") + } + + ensureHeaders(xsrfToken, req) + + // Stay in the loop until we get something else other than "pending". + // jp.Status can be: + // * accepted + // * expired + // * denied + + for jp.Status == "pending" { + time.Sleep(100 * time.Millisecond) + + resp, err := jc.client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "error retrieving verify response") + } + defer resp.Body.Close() + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal JumpCloud Protect body") + } + + if err := json.Unmarshal(bytes, &jp); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal poll result json into struct") + } + } + + if jp.Status != "accepted" { + return nil, errors.New(fmt.Sprintf("didn't receive accepted, status=%s\n", jp.Status)) + } + + jumpCloudParsedURL.Path = path.Join(jumpCloudParsedURL.Path, "login") + req, err = http.NewRequest("POST", jumpCloudParsedURL.String(), emptyJSONIOReader()) + if err != nil { + return nil, errors.Wrap(err, "failed to build JumpCoud login request") + } + + ensureHeaders(xsrfToken, req) + + return jc.client.Do(req) +} diff --git a/pkg/provider/jumpcloud/utils.go b/pkg/provider/jumpcloud/utils.go new file mode 100644 index 000000000..ffd075725 --- /dev/null +++ b/pkg/provider/jumpcloud/utils.go @@ -0,0 +1,17 @@ +package jumpcloud + +import ( + "bytes" + "io" + "net/http" +) + +func ensureHeaders(xsrfToken string, req *http.Request) { + req.Header.Add("X-Xsrftoken", xsrfToken) + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") +} + +func emptyJSONIOReader() io.Reader { + return bytes.NewReader([]byte(`{}`)) +} diff --git a/saml2aws.go b/saml2aws.go index 861f8c104..010c42aca 100644 --- a/saml2aws.go +++ b/saml2aws.go @@ -36,7 +36,7 @@ var MFAsByProvider = ProviderList{ "ADFS2": []string{"Auto", "RSA"}, // nothing automatic about ADFS 2.x "Ping": []string{"Auto"}, // automatically detects PingID "PingOne": []string{"Auto"}, // automatically detects PingID - "JumpCloud": []string{"Auto", "TOTP", "WEBAUTHN", "DUO"}, + "JumpCloud": []string{"Auto", "TOTP", "WEBAUTHN", "DUO", "PUSH"}, "Okta": []string{"Auto", "PUSH", "DUO", "SMS", "TOTP", "OKTA", "FIDO", "YUBICO TOKEN:HARDWARE"}, // automatically detects DUO, SMS, ToTP, and FIDO "OneLogin": []string{"Auto", "OLP", "SMS", "TOTP", "YUBIKEY"}, // automatically detects OneLogin Protect, SMS and ToTP "KeyCloak": []string{"Auto"}, // automatically detects ToTP