github.com/versent/saml2aws@v2.17.0+incompatible/pkg/provider/shibboleth/shibboleth.go (about) 1 package shibboleth 2 3 import ( 4 "crypto/tls" 5 "fmt" 6 "html" 7 "io/ioutil" 8 "net/http" 9 "net/url" 10 "regexp" 11 "strings" 12 "time" 13 14 "github.com/PuerkitoBio/goquery" 15 "github.com/pkg/errors" 16 "github.com/sirupsen/logrus" 17 "github.com/tidwall/gjson" 18 "github.com/versent/saml2aws/pkg/cfg" 19 "github.com/versent/saml2aws/pkg/creds" 20 "github.com/versent/saml2aws/pkg/prompter" 21 "github.com/versent/saml2aws/pkg/provider" 22 ) 23 24 var logger = logrus.WithField("provider", "shibboleth") 25 26 // Client wrapper around Shibboleth enabling authentication and retrieval of assertions 27 type Client struct { 28 client *provider.HTTPClient 29 idpAccount *cfg.IDPAccount 30 } 31 32 // New create a new Shibboleth client 33 func New(idpAccount *cfg.IDPAccount) (*Client, error) { 34 35 tr := &http.Transport{ 36 Proxy: http.ProxyFromEnvironment, 37 TLSClientConfig: &tls.Config{InsecureSkipVerify: idpAccount.SkipVerify, Renegotiation: tls.RenegotiateFreelyAsClient}, 38 } 39 40 client, err := provider.NewHTTPClient(tr) 41 if err != nil { 42 return nil, errors.Wrap(err, "error building http client") 43 } 44 45 return &Client{ 46 client: client, 47 idpAccount: idpAccount, 48 }, nil 49 } 50 51 // Authenticate authenticate to Shibboleth and return the data from the body of the SAML assertion. 52 func (sc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { 53 54 var authSubmitURL string 55 var samlAssertion string 56 57 shibbolethURL := fmt.Sprintf("%s/idp/profile/SAML2/Unsolicited/SSO?providerId=%s", loginDetails.URL, sc.idpAccount.AmazonWebservicesURN) 58 59 res, err := sc.client.Get(shibbolethURL) 60 if err != nil { 61 return samlAssertion, errors.Wrap(err, "error retrieving form") 62 } 63 64 doc, err := goquery.NewDocumentFromResponse(res) 65 if err != nil { 66 return samlAssertion, errors.Wrap(err, "failed to build document from response") 67 } 68 69 authForm := url.Values{} 70 71 doc.Find("input").Each(func(i int, s *goquery.Selection) { 72 updateFormData(authForm, s, loginDetails) 73 }) 74 75 doc.Find("form").Each(func(i int, s *goquery.Selection) { 76 action, ok := s.Attr("action") 77 if !ok { 78 return 79 } 80 authSubmitURL = action 81 }) 82 83 if authSubmitURL == "" { 84 return samlAssertion, fmt.Errorf("unable to locate IDP authentication form submit URL") 85 } 86 87 req, err := http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode())) 88 if err != nil { 89 return samlAssertion, errors.Wrap(err, "error building authentication request") 90 } 91 92 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 93 req.URL.Host = res.Request.URL.Host 94 req.URL.Scheme = res.Request.URL.Scheme 95 96 res, err = sc.client.Do(req) 97 if err != nil { 98 return samlAssertion, errors.Wrap(err, "error retrieving login form results") 99 } 100 101 switch sc.idpAccount.MFA { 102 case "Auto": 103 b, _ := ioutil.ReadAll(res.Body) 104 105 mfaRes, err := verifyMfa(sc, loginDetails.URL, string(b)) 106 if err != nil { 107 return mfaRes.Status, errors.Wrap(err, "error verifying MFA") 108 } 109 110 res = mfaRes 111 112 } 113 114 samlAssertion, err = extractSamlResponse(res) 115 if err != nil { 116 return samlAssertion, errors.Wrap(err, "error extracting SAMLResponse blob from final Shibboleth response") 117 } 118 119 return samlAssertion, nil 120 } 121 122 func updateFormData(authForm url.Values, s *goquery.Selection, user *creds.LoginDetails) { 123 name, ok := s.Attr("name") 124 authForm.Add("_eventId_proceed", "") 125 126 if !ok { 127 return 128 } 129 lname := strings.ToLower(name) 130 if strings.Contains(lname, "user") { 131 authForm.Add(name, user.Username) 132 } else if strings.Contains(lname, "email") { 133 authForm.Add(name, user.Username) 134 } else if strings.Contains(lname, "pass") { 135 authForm.Add(name, user.Password) 136 } else { 137 // pass through any hidden fields 138 val, ok := s.Attr("value") 139 if !ok { 140 return 141 } 142 authForm.Add(name, val) 143 } 144 } 145 146 func verifyMfa(oc *Client, shibbolethHost string, resp string) (*http.Response, error) { 147 148 duoHost, postAction, tx, app := parseTokens(resp) 149 150 parent := fmt.Sprintf(shibbolethHost + postAction) 151 152 duoTxCookie, err := verifyDuoMfa(oc, duoHost, parent, tx) 153 if err != nil { 154 return nil, errors.Wrap(err, "error when interacting with Duo iframe") 155 } 156 157 idpForm := url.Values{} 158 idpForm.Add("_eventId", "proceed") 159 idpForm.Add("sig_response", duoTxCookie+":"+app) 160 161 req, err := http.NewRequest("POST", parent, strings.NewReader(idpForm.Encode())) 162 if err != nil { 163 return nil, errors.Wrap(err, "error posting multi-factor verification to shibboleth server") 164 } 165 166 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 167 168 res, err := oc.client.Do(req) 169 if err != nil { 170 return nil, errors.Wrap(err, "error retrieving verify response") 171 } 172 173 return res, nil 174 } 175 176 func verifyDuoMfa(oc *Client, duoHost string, parent string, tx string) (string, error) { 177 // initiate duo mfa to get sid 178 duoSubmitURL := fmt.Sprintf("https://%s/frame/web/v1/auth", duoHost) 179 180 duoForm := url.Values{} 181 duoForm.Add("parent", parent) 182 duoForm.Add("java_version", "") 183 duoForm.Add("java_version", "") 184 duoForm.Add("flash_version", "") 185 duoForm.Add("screen_resolution_width", "3008") 186 duoForm.Add("screen_resolution_height", "1692") 187 duoForm.Add("color_depth", "24") 188 189 req, err := http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 190 if err != nil { 191 return "", errors.Wrap(err, "error building authentication request") 192 } 193 q := req.URL.Query() 194 q.Add("tx", tx) 195 req.URL.RawQuery = q.Encode() 196 197 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 198 199 res, err := oc.client.Do(req) 200 if err != nil { 201 return "", errors.Wrap(err, "error retrieving verify response") 202 } 203 204 //try to extract sid 205 doc, err := goquery.NewDocumentFromResponse(res) 206 if err != nil { 207 return "", errors.Wrap(err, "error parsing document") 208 } 209 210 duoSID, ok := doc.Find("input[name=\"sid\"]").Attr("value") 211 if !ok { 212 return "", errors.Wrap(err, "unable to locate saml response") 213 } 214 duoSID = html.UnescapeString(duoSID) 215 216 //prompt for mfa type 217 //supporting push, call, and passcode for now 218 219 var token string 220 221 var duoMfaOptions = []string{ 222 "Duo Push", 223 "Phone Call", 224 "Passcode", 225 } 226 227 duoMfaOption := prompter.Choose("Select a DUO MFA Option", duoMfaOptions) 228 229 if duoMfaOptions[duoMfaOption] == "Passcode" { 230 //get users DUO MFA Token 231 token = prompter.StringRequired("Enter passcode") 232 } 233 234 // send mfa auth request 235 duoSubmitURL = fmt.Sprintf("https://%s/frame/prompt", duoHost) 236 237 duoForm = url.Values{} 238 duoForm.Add("sid", duoSID) 239 duoForm.Add("device", "phone1") 240 duoForm.Add("factor", duoMfaOptions[duoMfaOption]) 241 duoForm.Add("out_of_date", "false") 242 if duoMfaOptions[duoMfaOption] == "Passcode" { 243 duoForm.Add("passcode", token) 244 } 245 246 req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 247 if err != nil { 248 return "", errors.Wrap(err, "error building authentication request") 249 } 250 251 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 252 253 res, err = oc.client.Do(req) 254 if err != nil { 255 return "", errors.Wrap(err, "error retrieving verify response") 256 } 257 258 body, err := ioutil.ReadAll(res.Body) 259 if err != nil { 260 return "", errors.Wrap(err, "error retrieving body from response") 261 } 262 263 resp := string(body) 264 265 duoTxStat := gjson.Get(resp, "stat").String() 266 duoTxID := gjson.Get(resp, "response.txid").String() 267 if duoTxStat != "OK" { 268 return "", errors.Wrap(err, "error authenticating mfa device") 269 } 270 271 // get duo cookie 272 duoSubmitURL = fmt.Sprintf("https://%s/frame/status", duoHost) 273 274 duoForm = url.Values{} 275 duoForm.Add("sid", duoSID) 276 duoForm.Add("txid", duoTxID) 277 278 req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 279 if err != nil { 280 return "", errors.Wrap(err, "error building authentication request") 281 } 282 283 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 284 285 res, err = oc.client.Do(req) 286 if err != nil { 287 return "", errors.Wrap(err, "error retrieving verify response") 288 } 289 290 body, err = ioutil.ReadAll(res.Body) 291 if err != nil { 292 return "", errors.Wrap(err, "error retrieving body from response") 293 } 294 295 resp = string(body) 296 297 duoTxResult := gjson.Get(resp, "response.result").String() 298 duoResultURL := gjson.Get(resp, "response.result_url").String() 299 300 fmt.Println(gjson.Get(resp, "response.status").String()) 301 302 if duoTxResult != "SUCCESS" { 303 //poll as this is likely a push request 304 for { 305 time.Sleep(3 * time.Second) 306 307 req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 308 if err != nil { 309 return "", errors.Wrap(err, "error building authentication request") 310 } 311 312 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 313 314 res, err = oc.client.Do(req) 315 if err != nil { 316 return "", errors.Wrap(err, "error retrieving verify response") 317 } 318 319 body, err = ioutil.ReadAll(res.Body) 320 if err != nil { 321 return "", errors.Wrap(err, "error retrieving body from response") 322 } 323 324 resp := string(body) 325 326 duoTxResult = gjson.Get(resp, "response.result").String() 327 duoResultURL = gjson.Get(resp, "response.result_url").String() 328 329 fmt.Println(gjson.Get(resp, "response.status").String()) 330 331 if duoTxResult == "FAILURE" { 332 return "", errors.Wrap(err, "failed to authenticate device") 333 } 334 335 if duoTxResult == "SUCCESS" { 336 break 337 } 338 } 339 } 340 341 duoRequestURL := fmt.Sprintf("https://%s%s", duoHost, duoResultURL) 342 req, err = http.NewRequest("POST", duoRequestURL, strings.NewReader(duoForm.Encode())) 343 if err != nil { 344 return "", errors.Wrap(err, "error constructing request object to result url") 345 } 346 347 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 348 349 res, err = oc.client.Do(req) 350 if err != nil { 351 return "", errors.Wrap(err, "error retrieving duo result response") 352 } 353 354 body, err = ioutil.ReadAll(res.Body) 355 if err != nil { 356 return "", errors.Wrap(err, "duoResultSubmit: error retrieving body from response") 357 } 358 359 resp = string(body) 360 361 duoTxCookie := gjson.Get(resp, "response.cookie").String() 362 if duoTxCookie == "" { 363 return "", errors.Wrap(err, "duoResultSubmit: Unable to get response.cookie") 364 } 365 366 return duoTxCookie, nil 367 } 368 369 func parseTokens(blob string) (string, string, string, string) { 370 hostRgx := regexp.MustCompile(`data-host=\"(.*?)\"`) 371 sigRgx := regexp.MustCompile(`data-sig-request=\"(.*?)\"`) 372 dpaRgx := regexp.MustCompile(`data-post-action=\"(.*?)\"`) 373 374 dataSigRequest := sigRgx.FindStringSubmatch(blob) 375 duoHost := hostRgx.FindStringSubmatch(blob) 376 postAction := dpaRgx.FindStringSubmatch(blob) 377 378 duoSignatures := strings.Split(dataSigRequest[1], ":") 379 return duoHost[1], postAction[1], duoSignatures[0], duoSignatures[1] 380 } 381 382 func extractSamlResponse(res *http.Response) (string, error) { 383 body, err := ioutil.ReadAll(res.Body) 384 if err != nil { 385 return "", errors.Wrap(err, "extractSamlResponse: error retrieving body from response") 386 } 387 388 samlRgx := regexp.MustCompile(`name=\"SAMLResponse\" value=\"(.*?)\"/>`) 389 samlResponseValue := samlRgx.FindStringSubmatch(string(body)) 390 return samlResponseValue[1], nil 391 }