github.com/versent/saml2aws@v2.17.0+incompatible/pkg/provider/jumpcloud/jumpcloud.go (about) 1 package jumpcloud 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "net/http" 9 "regexp" 10 "strings" 11 12 "github.com/PuerkitoBio/goquery" 13 "github.com/pkg/errors" 14 "github.com/versent/saml2aws/pkg/cfg" 15 "github.com/versent/saml2aws/pkg/creds" 16 "github.com/versent/saml2aws/pkg/prompter" 17 "github.com/versent/saml2aws/pkg/provider" 18 ) 19 20 const ( 21 jcSSOBaseURL = "https://sso.jumpcloud.com/" 22 xsrfURL = "https://console.jumpcloud.com/userconsole/xsrf" 23 authSubmitURL = "https://console.jumpcloud.com/userconsole/auth" 24 ) 25 26 // Client is a wrapper representing a JumpCloud SAML client 27 type Client struct { 28 client *provider.HTTPClient 29 } 30 31 // XSRF is for unmarshalling the xsrf token in the response 32 type XSRF struct { 33 Token string `json:"xsrf"` 34 } 35 36 // AuthRequest is to be sent to JumpCloud as the auth req body 37 type AuthRequest struct { 38 Context string 39 RedirectTo string 40 Email string 41 Password string 42 OTP string 43 } 44 45 // JCRedirect is for unmarshalling the redirect address from the response after the auth 46 type JCRedirect struct { 47 Address string `json:"redirectTo"` 48 } 49 50 // New creates a new JumpCloud client 51 func New(idpAccount *cfg.IDPAccount) (*Client, error) { 52 53 tr := provider.NewDefaultTransport(idpAccount.SkipVerify) 54 55 client, err := provider.NewHTTPClient(tr) 56 if err != nil { 57 return nil, errors.Wrap(err, "error building http client") 58 } 59 60 return &Client{ 61 client: client, 62 }, nil 63 } 64 65 // Authenticate logs into JumpCloud and returns a SAML response 66 func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { 67 var samlAssertion string 68 var a AuthRequest 69 re := regexp.MustCompile(jcSSOBaseURL) 70 71 // Start by getting the XSRF Token 72 res, err := jc.client.Get(xsrfURL) 73 if err != nil { 74 return samlAssertion, errors.Wrap(err, "error retieving XSRF Token") 75 } 76 77 // Grab the web response that has the xsrf in it 78 xsrfBody, err := ioutil.ReadAll(res.Body) 79 80 // Unmarshall the answer and store the token 81 var x = new(XSRF) 82 err = json.Unmarshal(xsrfBody, &x) 83 if err != nil { 84 log.Fatalf("Error unmarshalling xsrf response! %v", err) 85 } 86 87 // Populate our Auth body for the POST 88 a.Context = "sso" 89 a.RedirectTo = re.ReplaceAllString(loginDetails.URL, "") 90 a.Email = loginDetails.Username 91 a.Password = loginDetails.Password 92 93 authBody, err := json.Marshal(a) 94 if err != nil { 95 return samlAssertion, errors.Wrap(err, "failed to build auth request body") 96 } 97 98 // Generate our auth request 99 req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(string(authBody))) 100 if err != nil { 101 return samlAssertion, errors.Wrap(err, "error building authentication request") 102 } 103 104 // Add the necessary headers to the auth request 105 req.Header.Add("X-Xsrftoken", x.Token) 106 req.Header.Add("Accept", "application/json") 107 req.Header.Add("Content-Type", "application/json") 108 109 res, err = jc.client.Do(req) 110 if err != nil { 111 return samlAssertion, errors.Wrap(err, "error retrieving login form") 112 } 113 114 // Check if we get a 401. If we did, MFA is required and the OTP was not provided. 115 // Get the OTP and resubmit. 116 if res.StatusCode == 401 { 117 // Get the user's MFA token and re-build the body 118 a.OTP = loginDetails.MFAToken 119 if a.OTP == "" { 120 a.OTP = prompter.StringRequired("MFA Token") 121 } 122 123 authBody, err = json.Marshal(a) 124 if err != nil { 125 return samlAssertion, errors.Wrap(err, "error building authentication req body after getting MFA Token") 126 } 127 128 // Re-request with our OTP 129 req, err = http.NewRequest("POST", authSubmitURL, strings.NewReader(string(authBody))) 130 if err != nil { 131 return samlAssertion, errors.Wrap(err, "error building MFA authentication request") 132 } 133 134 // Re-add the necessary headers to our remade auth request 135 req.Header.Add("X-Xsrftoken", x.Token) 136 req.Header.Add("Accept", "application/json") 137 req.Header.Add("Content-Type", "application/json") 138 139 // Resubmit 140 res, err = jc.client.Do(req) 141 if err != nil { 142 return samlAssertion, errors.Wrap(err, "error submitting MFA login form") 143 } 144 } 145 146 // Check if our auth was successful 147 if res.StatusCode == 200 { 148 // Grab the body from the response that has the redirect in it. 149 reDirBody, err := ioutil.ReadAll(res.Body) 150 151 // Unmarshall the body to get the redirect address 152 var jcrd = new(JCRedirect) 153 err = json.Unmarshal(reDirBody, &jcrd) 154 if err != nil { 155 log.Fatalf("Error unmarshalling redirectTo response! %v", err) 156 } 157 158 // Send the final GET for our SAML response 159 res, err = jc.client.Get(jcrd.Address) 160 if err != nil { 161 return samlAssertion, errors.Wrap(err, "error submitting request for SAML value") 162 } 163 164 //try to extract SAMLResponse 165 doc, err := goquery.NewDocumentFromReader(res.Body) 166 if err != nil { 167 return samlAssertion, errors.Wrap(err, "error parsing document") 168 } 169 170 doc.Find("input").Each(func(i int, s *goquery.Selection) { 171 name, ok := s.Attr("name") 172 if !ok { 173 log.Fatalf("unable to locate IDP authentication form submit URL") 174 } 175 176 if name == "SAMLResponse" { 177 val, ok := s.Attr("value") 178 if !ok { 179 log.Fatalf("unable to locate saml assertion value") 180 } 181 samlAssertion = val 182 } 183 184 }) 185 186 } else { 187 errMsg := fmt.Sprintf("error when trying to auth, status code %d", res.StatusCode) 188 return samlAssertion, errors.Wrap(err, errMsg) 189 } 190 191 return samlAssertion, nil 192 }