Skip to content

Commit

Permalink
Add JumpCloud Protect (PUSH) MFA support
Browse files Browse the repository at this point in the history
JumpCloud have released a new app that they call [JumpCloud
Protect](https://jumpcloud.com/press/introducing-jumpcloud-protect-free-mobile-multi-factor-authentication).

It is a free alternative to DUO for JumpCoud users that provides "push"
second factor. It would be useful to add this in if possible (we are
planning to use it).
  • Loading branch information
Murat Mukhtarov committed Sep 8, 2021
1 parent f3d590d commit 1ffd06b
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 14 deletions.
31 changes: 18 additions & 13 deletions pkg/provider/jumpcloud/jumpcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
}
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
103 changes: 103 additions & 0 deletions pkg/provider/jumpcloud/jumpcloud_protect.go
Original file line number Diff line number Diff line change
@@ -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)
}
17 changes: 17 additions & 0 deletions pkg/provider/jumpcloud/utils.go
Original file line number Diff line number Diff line change
@@ -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(`{}`))
}
2 changes: 1 addition & 1 deletion saml2aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1ffd06b

Please sign in to comment.