github.com/versent/saml2aws@v2.17.0+incompatible/pkg/provider/okta/okta.go (about) 1 package okta 2 3 import ( 4 "bytes" 5 "fmt" 6 "html" 7 "io/ioutil" 8 "net/http" 9 "net/url" 10 "strings" 11 "time" 12 13 "github.com/sirupsen/logrus" 14 "github.com/versent/saml2aws/pkg/prompter" 15 16 "github.com/PuerkitoBio/goquery" 17 "github.com/pkg/errors" 18 "github.com/tidwall/gjson" 19 "github.com/versent/saml2aws/pkg/cfg" 20 "github.com/versent/saml2aws/pkg/creds" 21 "github.com/versent/saml2aws/pkg/provider" 22 23 "encoding/json" 24 ) 25 26 const ( 27 IdentifierDuoMfa = "DUO WEB" 28 IdentifierSmsMfa = "OKTA SMS" 29 IdentifierPushMfa = "OKTA PUSH" 30 IdentifierTotpMfa = "GOOGLE TOKEN:SOFTWARE:TOTP" 31 IdentifierOktaTotpMfa = "OKTA TOKEN:SOFTWARE:TOTP" 32 ) 33 34 var logger = logrus.WithField("provider", "okta") 35 36 var ( 37 supportedMfaOptions = map[string]string{ 38 IdentifierDuoMfa: "DUO MFA authentication", 39 IdentifierSmsMfa: "SMS MFA authentication", 40 IdentifierPushMfa: "PUSH MFA authentication", 41 IdentifierTotpMfa: "TOTP MFA authentication", 42 IdentifierOktaTotpMfa: "Okta MFA authentication", 43 } 44 ) 45 46 // Client is a wrapper representing a Okta SAML client 47 type Client struct { 48 client *provider.HTTPClient 49 mfa string 50 } 51 52 // AuthRequest represents an mfa okta request 53 type AuthRequest struct { 54 Username string `json:"username"` 55 Password string `json:"password"` 56 } 57 58 // VerifyRequest represents an mfa verify request 59 type VerifyRequest struct { 60 StateToken string `json:"stateToken"` 61 PassCode string `json:"passCode,omitempty"` 62 } 63 64 // New creates a new Okta client 65 func New(idpAccount *cfg.IDPAccount) (*Client, error) { 66 67 tr := provider.NewDefaultTransport(idpAccount.SkipVerify) 68 69 client, err := provider.NewHTTPClient(tr) 70 if err != nil { 71 return nil, errors.Wrap(err, "error building http client") 72 } 73 74 // assign a response validator to ensure all responses are either success or a redirect 75 // this is to avoid have explicit checks for every single response 76 client.CheckResponseStatus = provider.SuccessOrRedirectResponseValidator 77 78 return &Client{ 79 client: client, 80 mfa: idpAccount.MFA, 81 }, nil 82 } 83 84 // Authenticate logs into Okta and returns a SAML response 85 func (oc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) { 86 87 var samlAssertion string 88 89 oktaURL, err := url.Parse(loginDetails.URL) 90 if err != nil { 91 return samlAssertion, errors.Wrap(err, "error building oktaURL") 92 } 93 94 oktaOrgHost := oktaURL.Host 95 96 //authenticate via okta api 97 authReq := AuthRequest{Username: loginDetails.Username, Password: loginDetails.Password} 98 authBody := new(bytes.Buffer) 99 err = json.NewEncoder(authBody).Encode(authReq) 100 if err != nil { 101 return samlAssertion, errors.Wrap(err, "error encoding authreq") 102 } 103 104 authSubmitURL := fmt.Sprintf("https://%s/api/v1/authn", oktaOrgHost) 105 106 req, err := http.NewRequest("POST", authSubmitURL, authBody) 107 if err != nil { 108 return samlAssertion, errors.Wrap(err, "error building authentication request") 109 } 110 111 req.Header.Add("Content-Type", "application/json") 112 req.Header.Add("Accept", "application/json") 113 114 res, err := oc.client.Do(req) 115 if err != nil { 116 return samlAssertion, errors.Wrap(err, "error retrieving auth response") 117 } 118 119 body, err := ioutil.ReadAll(res.Body) 120 if err != nil { 121 return samlAssertion, errors.Wrap(err, "error retrieving body from response") 122 } 123 124 resp := string(body) 125 126 authStatus := gjson.Get(resp, "status").String() 127 oktaSessionToken := gjson.Get(resp, "sessionToken").String() 128 129 // mfa required 130 if authStatus == "MFA_REQUIRED" { 131 oktaSessionToken, err = verifyMfa(oc, oktaOrgHost, loginDetails, resp) 132 if err != nil { 133 return samlAssertion, errors.Wrap(err, "error verifying MFA") 134 } 135 } 136 137 //now call saml endpoint 138 oktaSessionRedirectURL := fmt.Sprintf("https://%s/login/sessionCookieRedirect", oktaOrgHost) 139 140 req, err = http.NewRequest("GET", oktaSessionRedirectURL, nil) 141 if err != nil { 142 return samlAssertion, errors.Wrap(err, "error building authentication request") 143 } 144 q := req.URL.Query() 145 q.Add("checkAccountSetupComplete", "true") 146 q.Add("token", oktaSessionToken) 147 q.Add("redirectUrl", loginDetails.URL) 148 req.URL.RawQuery = q.Encode() 149 150 res, err = oc.client.Do(req) 151 if err != nil { 152 return samlAssertion, errors.Wrap(err, "error retrieving verify response") 153 } 154 155 //try to extract SAMLResponse 156 doc, err := goquery.NewDocumentFromResponse(res) 157 if err != nil { 158 return samlAssertion, errors.Wrap(err, "error parsing document") 159 } 160 161 samlAssertion, ok := doc.Find("input[name=\"SAMLResponse\"]").Attr("value") 162 if !ok { 163 return samlAssertion, errors.Wrap(err, "unable to locate saml response") 164 } 165 166 logger.Debug("auth complete") 167 168 return samlAssertion, nil 169 } 170 171 func parseMfaIdentifer(json string, arrayPosition int) string { 172 mfaProvider := gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.provider", arrayPosition)).String() 173 factorType := strings.ToUpper(gjson.Get(json, fmt.Sprintf("_embedded.factors.%d.factorType", arrayPosition)).String()) 174 return fmt.Sprintf("%s %s", mfaProvider, factorType) 175 } 176 177 func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, resp string) (string, error) { 178 179 stateToken := gjson.Get(resp, "stateToken").String() 180 181 // choose an mfa option if there are multiple enabled 182 mfaOption := 0 183 var mfaOptions []string 184 for i := range gjson.Get(resp, "_embedded.factors").Array() { 185 identifier := parseMfaIdentifer(resp, i) 186 if val, ok := supportedMfaOptions[identifier]; ok { 187 mfaOptions = append(mfaOptions, val) 188 } else { 189 mfaOptions = append(mfaOptions, "UNSUPPORTED: "+identifier) 190 } 191 } 192 193 if oc.mfa != "AUTO" { 194 for _, val := range mfaOptions { 195 if strings.HasPrefix(val, oc.mfa) { 196 mfaOptions = []string{val} 197 break 198 } 199 } 200 } 201 if len(mfaOptions) > 1 { 202 mfaOption = prompter.Choose("Select which MFA option to use", mfaOptions) 203 } 204 205 factorID := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d.id", mfaOption)).String() 206 oktaVerify := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d._links.verify.href", mfaOption)).String() 207 mfaIdentifer := parseMfaIdentifer(resp, mfaOption) 208 209 logger.WithField("factorID", factorID).WithField("oktaVerify", oktaVerify).WithField("mfaIdentifer", mfaIdentifer).Debug("MFA") 210 211 if _, ok := supportedMfaOptions[mfaIdentifer]; !ok { 212 return "", errors.New("unsupported mfa provider") 213 } 214 215 // get signature & callback 216 verifyReq := VerifyRequest{StateToken: stateToken} 217 verifyBody := new(bytes.Buffer) 218 err := json.NewEncoder(verifyBody).Encode(verifyReq) 219 if err != nil { 220 return "", errors.Wrap(err, "error encoding verifyReq") 221 } 222 223 req, err := http.NewRequest("POST", oktaVerify, verifyBody) 224 if err != nil { 225 return "", errors.Wrap(err, "error building verify request") 226 } 227 228 req.Header.Add("Content-Type", "application/json") 229 req.Header.Add("Accept", "application/json") 230 231 res, err := oc.client.Do(req) 232 if err != nil { 233 return "", errors.Wrap(err, "error retrieving verify response") 234 } 235 236 body, err := ioutil.ReadAll(res.Body) 237 if err != nil { 238 return "", errors.Wrap(err, "error retrieving body from response") 239 } 240 resp = string(body) 241 242 switch mfa := mfaIdentifer; mfa { 243 case IdentifierSmsMfa, IdentifierTotpMfa, IdentifierOktaTotpMfa: 244 verifyCode := prompter.StringRequired("Enter verification code") 245 tokenReq := VerifyRequest{StateToken: stateToken, PassCode: verifyCode} 246 tokenBody := new(bytes.Buffer) 247 json.NewEncoder(tokenBody).Encode(tokenReq) 248 249 req, err = http.NewRequest("POST", oktaVerify, tokenBody) 250 if err != nil { 251 return "", errors.Wrap(err, "error building token post request") 252 } 253 254 req.Header.Add("Content-Type", "application/json") 255 req.Header.Add("Accept", "application/json") 256 257 res, err := oc.client.Do(req) 258 if err != nil { 259 return "", errors.Wrap(err, "error retrieving token post response") 260 } 261 262 body, err := ioutil.ReadAll(res.Body) 263 if err != nil { 264 return "", errors.Wrap(err, "error retrieving body from response") 265 } 266 267 resp = string(body) 268 269 return gjson.Get(resp, "sessionToken").String(), nil 270 271 case IdentifierPushMfa: 272 273 fmt.Printf("\nWaiting for approval, please check your Okta Verify app ...") 274 275 // loop until success, error, or timeout 276 for { 277 278 res, err = oc.client.Do(req) 279 if err != nil { 280 return "", errors.Wrap(err, "error retrieving verify response") 281 } 282 283 body, err = ioutil.ReadAll(res.Body) 284 if err != nil { 285 return "", errors.Wrap(err, "error retrieving body from response") 286 } 287 288 // on 'success' status 289 if gjson.Get(string(body), "status").String() == "SUCCESS" { 290 fmt.Printf(" Approved\n\n") 291 return gjson.Get(string(body), "sessionToken").String(), nil 292 } 293 294 // otherwise probably still waiting 295 switch gjson.Get(string(body), "factorResult").String() { 296 297 case "WAITING": 298 time.Sleep(1000) 299 fmt.Printf(".") 300 logger.Debug("Waiting for user to authorize login") 301 302 case "TIMEOUT": 303 fmt.Printf(" Timeout\n") 304 return "", errors.New("User did not accept MFA in time") 305 306 case "REJECTED": 307 fmt.Printf(" Rejected\n") 308 return "", errors.New("MFA rejected by user") 309 310 default: 311 fmt.Printf(" Error\n") 312 return "", errors.New("Unsupported response from Okta, please raise ticket with saml2aws") 313 314 } 315 316 } 317 318 case IdentifierDuoMfa: 319 duoHost := gjson.Get(resp, "_embedded.factor._embedded.verification.host").String() 320 duoSignature := gjson.Get(resp, "_embedded.factor._embedded.verification.signature").String() 321 duoSiguatres := strings.Split(duoSignature, ":") 322 //duoSignatures[0] = TX 323 //duoSignatures[1] = APP 324 duoCallback := gjson.Get(resp, "_embedded.factor._embedded.verification._links.complete.href").String() 325 326 // initiate duo mfa to get sid 327 duoSubmitURL := fmt.Sprintf("https://%s/frame/web/v1/auth", duoHost) 328 329 duoForm := url.Values{} 330 duoForm.Add("parent", fmt.Sprintf("https://%s/signin/verify/duo/web", oktaOrgHost)) 331 duoForm.Add("java_version", "") 332 duoForm.Add("java_version", "") 333 duoForm.Add("flash_version", "") 334 duoForm.Add("screen_resolution_width", "3008") 335 duoForm.Add("screen_resolution_height", "1692") 336 duoForm.Add("color_depth", "24") 337 338 req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 339 if err != nil { 340 return "", errors.Wrap(err, "error building authentication request") 341 } 342 q := req.URL.Query() 343 q.Add("tx", duoSiguatres[0]) 344 req.URL.RawQuery = q.Encode() 345 346 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 347 348 res, err = oc.client.Do(req) 349 if err != nil { 350 return "", errors.Wrap(err, "error retrieving verify response") 351 } 352 353 //try to extract sid 354 doc, err := goquery.NewDocumentFromResponse(res) 355 if err != nil { 356 return "", errors.Wrap(err, "error parsing document") 357 } 358 359 duoSID, ok := doc.Find("input[name=\"sid\"]").Attr("value") 360 if !ok { 361 return "", errors.Wrap(err, "unable to locate saml response") 362 } 363 duoSID = html.UnescapeString(duoSID) 364 365 //prompt for mfa type 366 //only supporting push or passcode for now 367 var token string 368 369 var duoMfaOptions = []string{ 370 "Duo Push", 371 "Passcode", 372 } 373 374 duoMfaOption := 0 375 376 if loginDetails.DuoMFAOption == "Duo Push" { 377 duoMfaOption = 0 378 } else if loginDetails.DuoMFAOption == "Passcode" { 379 duoMfaOption = 1 380 } else { 381 duoMfaOption = prompter.Choose("Select a DUO MFA Option", duoMfaOptions) 382 } 383 384 if duoMfaOptions[duoMfaOption] == "Passcode" { 385 //get users DUO MFA Token 386 token = prompter.StringRequired("Enter passcode") 387 } 388 389 // send mfa auth request 390 duoSubmitURL = fmt.Sprintf("https://%s/frame/prompt", duoHost) 391 392 duoForm = url.Values{} 393 duoForm.Add("sid", duoSID) 394 duoForm.Add("device", "phone1") 395 duoForm.Add("factor", duoMfaOptions[duoMfaOption]) 396 duoForm.Add("out_of_date", "false") 397 if duoMfaOptions[duoMfaOption] == "Passcode" { 398 duoForm.Add("passcode", token) 399 } 400 401 req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 402 if err != nil { 403 return "", errors.Wrap(err, "error building authentication request") 404 } 405 406 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 407 408 res, err = oc.client.Do(req) 409 if err != nil { 410 return "", errors.Wrap(err, "error retrieving verify response") 411 } 412 413 body, err = ioutil.ReadAll(res.Body) 414 if err != nil { 415 return "", errors.Wrap(err, "error retrieving body from response") 416 } 417 418 resp = string(body) 419 420 duoTxStat := gjson.Get(resp, "stat").String() 421 duoTxID := gjson.Get(resp, "response.txid").String() 422 if duoTxStat != "OK" { 423 return "", errors.Wrap(err, "error authenticating mfa device") 424 } 425 426 // get duo cookie 427 duoSubmitURL = fmt.Sprintf("https://%s/frame/status", duoHost) 428 429 duoForm = url.Values{} 430 duoForm.Add("sid", duoSID) 431 duoForm.Add("txid", duoTxID) 432 433 req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 434 if err != nil { 435 return "", errors.Wrap(err, "error building authentication request") 436 } 437 438 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 439 440 res, err = oc.client.Do(req) 441 if err != nil { 442 return "", errors.Wrap(err, "error retrieving verify response") 443 } 444 445 body, err = ioutil.ReadAll(res.Body) 446 if err != nil { 447 return "", errors.Wrap(err, "error retrieving body from response") 448 } 449 450 resp = string(body) 451 452 duoTxResult := gjson.Get(resp, "response.result").String() 453 duoResultURL := gjson.Get(resp, "response.result_url").String() 454 455 fmt.Println(gjson.Get(resp, "response.status").String()) 456 457 if duoTxResult != "SUCCESS" { 458 //poll as this is likely a push request 459 for { 460 time.Sleep(3 * time.Second) 461 462 req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode())) 463 if err != nil { 464 return "", errors.Wrap(err, "error building authentication request") 465 } 466 467 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 468 469 res, err = oc.client.Do(req) 470 if err != nil { 471 return "", errors.Wrap(err, "error retrieving verify response") 472 } 473 474 body, err = ioutil.ReadAll(res.Body) 475 if err != nil { 476 return "", errors.Wrap(err, "error retrieving body from response") 477 } 478 479 resp := string(body) 480 481 duoTxResult = gjson.Get(resp, "response.result").String() 482 duoResultURL = gjson.Get(resp, "response.result_url").String() 483 484 fmt.Println(gjson.Get(resp, "response.status").String()) 485 486 if duoTxResult == "FAILURE" { 487 return "", errors.Wrap(err, "failed to authenticate device") 488 } 489 490 if duoTxResult == "SUCCESS" { 491 break 492 } 493 } 494 } 495 496 duoRequestURL := fmt.Sprintf("https://%s%s", duoHost, duoResultURL) 497 req, err = http.NewRequest("POST", duoRequestURL, strings.NewReader(duoForm.Encode())) 498 if err != nil { 499 return "", errors.Wrap(err, "error constructing request object to result url") 500 } 501 502 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 503 504 res, err = oc.client.Do(req) 505 if err != nil { 506 return "", errors.Wrap(err, "error retrieving duo result response") 507 } 508 509 body, err = ioutil.ReadAll(res.Body) 510 if err != nil { 511 return "", errors.Wrap(err, "duoResultSubmit: error retrieving body from response") 512 } 513 514 resp := string(body) 515 duoTxCookie := gjson.Get(resp, "response.cookie").String() 516 if duoTxCookie == "" { 517 return "", errors.Wrap(err, "duoResultSubmit: Unable to get response.cookie") 518 } 519 520 // callback to okta with cookie 521 oktaForm := url.Values{} 522 oktaForm.Add("id", factorID) 523 oktaForm.Add("stateToken", stateToken) 524 oktaForm.Add("sig_response", fmt.Sprintf("%s:%s", duoTxCookie, duoSiguatres[1])) 525 526 req, err = http.NewRequest("POST", duoCallback, strings.NewReader(oktaForm.Encode())) 527 if err != nil { 528 return "", errors.Wrap(err, "error building authentication request") 529 } 530 531 req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 532 533 res, err = oc.client.Do(req) 534 if err != nil { 535 return "", errors.Wrap(err, "error retrieving verify response") 536 } 537 538 // extract okta session token 539 540 verifyReq = VerifyRequest{StateToken: stateToken} 541 verifyBody = new(bytes.Buffer) 542 json.NewEncoder(verifyBody).Encode(verifyReq) 543 544 req, err = http.NewRequest("POST", oktaVerify, verifyBody) 545 if err != nil { 546 return "", errors.Wrap(err, "error building verify request") 547 } 548 549 req.Header.Add("Content-Type", "application/json") 550 req.Header.Add("Accept", "application/json") 551 req.Header.Add("X-Okta-XsrfToken", "") 552 553 res, err = oc.client.Do(req) 554 if err != nil { 555 return "", errors.Wrap(err, "error retrieving verify response") 556 } 557 558 body, err = ioutil.ReadAll(res.Body) 559 if err != nil { 560 return "", errors.Wrap(err, "error retrieving body from response") 561 } 562 563 return gjson.GetBytes(body, "sessionToken").String(), nil 564 } 565 566 // catch all 567 return "", errors.New("no mfa options provided") 568 }