Skip to content

Commit cd49354

Browse files
author
Mark Wolfe
authored
Merge pull request #630 from Versent/dustinblackman-okta-multi-fido
dustinblackman okta multi fido
2 parents eb7bc3c + 794281d commit cd49354

File tree

1 file changed

+147
-88
lines changed

1 file changed

+147
-88
lines changed

pkg/provider/okta/okta.go

Lines changed: 147 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"time"
1818

1919
"github.com/PuerkitoBio/goquery"
20+
"github.com/marshallbrekka/go-u2fhost"
2021
"github.com/pkg/errors"
2122
"github.com/sirupsen/logrus"
2223
"github.com/tidwall/gjson"
@@ -75,6 +76,14 @@ type VerifyRequest struct {
7576
PassCode string `json:"passCode,omitempty"`
7677
}
7778

79+
// mfaChallengeContext is used to hold MFA challenge context in a simple struct.
80+
type mfaChallengeContext struct {
81+
factorID string
82+
oktaVerify string
83+
mfaIdentifer string
84+
challengeResponseBody string
85+
}
86+
7887
// New creates a new Okta client
7988
func New(idpAccount *cfg.IDPAccount) (*Client, error) {
8089

@@ -306,41 +315,28 @@ func extractSAMLResponse(doc *goquery.Document) (v string, ok bool) {
306315
return doc.Find("input[name=\"SAMLResponse\"]").Attr("value")
307316
}
308317

309-
func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, resp string) (string, error) {
310-
311-
stateToken := gjson.Get(resp, "stateToken").String()
312-
313-
// choose an mfa option if there are multiple enabled
314-
mfaOption := 0
315-
var mfaOptions []string
316-
for i := range gjson.Get(resp, "_embedded.factors").Array() {
317-
identifier := parseMfaIdentifer(resp, i)
318-
if val, ok := supportedMfaOptions[identifier]; ok {
319-
mfaOptions = append(mfaOptions, val)
320-
} else {
321-
mfaOptions = append(mfaOptions, "UNSUPPORTED: "+identifier)
318+
func findMfaOption(mfa string, mfaOptions []string, startAtIdx int) int {
319+
for idx, val := range mfaOptions {
320+
if startAtIdx >= idx {
321+
continue
322322
}
323-
}
324-
325-
if strings.ToUpper(oc.mfa) != "AUTO" {
326-
for idx, val := range mfaOptions {
327-
if strings.HasPrefix(strings.ToUpper(val), oc.mfa) {
328-
mfaOption = idx
329-
break
330-
}
323+
if strings.HasPrefix(strings.ToUpper(val), mfa) {
324+
return idx
331325
}
332-
} else if len(mfaOptions) > 1 {
333-
mfaOption = prompter.Choose("Select which MFA option to use", mfaOptions)
334326
}
327+
return 0
328+
}
335329

330+
func getMfaChallengeContext(oc *Client, mfaOption int, resp string) (*mfaChallengeContext, error) {
331+
stateToken := gjson.Get(resp, "stateToken").String()
336332
factorID := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d.id", mfaOption)).String()
337333
oktaVerify := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d._links.verify.href", mfaOption)).String()
338334
mfaIdentifer := parseMfaIdentifer(resp, mfaOption)
339335

340336
logger.WithField("factorID", factorID).WithField("oktaVerify", oktaVerify).WithField("mfaIdentifer", mfaIdentifer).Debug("MFA")
341337

342338
if _, ok := supportedMfaOptions[mfaIdentifer]; !ok {
343-
return "", errors.New("unsupported mfa provider")
339+
return nil, errors.New("unsupported mfa provider")
344340
}
345341

346342
// get signature & callback
@@ -359,31 +355,64 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
359355

360356
err := json.NewEncoder(verifyBody).Encode(verifyReq)
361357
if err != nil {
362-
return "", errors.Wrap(err, "error encoding verifyReq")
358+
return nil, errors.Wrap(err, "error encoding verifyReq")
363359
}
364360

365361
req, err := http.NewRequest("POST", oktaVerify, verifyBody)
366362
if err != nil {
367-
return "", errors.Wrap(err, "error building verify request")
363+
return nil, errors.Wrap(err, "error building verify request")
368364
}
369365

370366
req.Header.Add("Content-Type", "application/json")
371367
req.Header.Add("Accept", "application/json")
372368

373369
res, err := oc.client.Do(req)
374370
if err != nil {
375-
return "", errors.Wrap(err, "error retrieving verify response")
371+
return nil, errors.Wrap(err, "error retrieving verify response")
376372
}
377373

378374
body, err := ioutil.ReadAll(res.Body)
379375
if err != nil {
380-
return "", errors.Wrap(err, "error retrieving body from response")
376+
return nil, errors.Wrap(err, "error retrieving body from response")
381377
}
382-
resp = string(body)
383378

384-
switch mfa := mfaIdentifer; mfa {
379+
return &mfaChallengeContext{
380+
factorID: factorID,
381+
oktaVerify: oktaVerify,
382+
mfaIdentifer: mfaIdentifer,
383+
challengeResponseBody: string(body),
384+
}, nil
385+
}
386+
387+
func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails, resp string) (string, error) {
388+
stateToken := gjson.Get(resp, "stateToken").String()
389+
390+
// choose an mfa option if there are multiple enabled
391+
mfaOption := 0
392+
var mfaOptions []string
393+
for i := range gjson.Get(resp, "_embedded.factors").Array() {
394+
identifier := parseMfaIdentifer(resp, i)
395+
if val, ok := supportedMfaOptions[identifier]; ok {
396+
mfaOptions = append(mfaOptions, val)
397+
} else {
398+
mfaOptions = append(mfaOptions, "UNSUPPORTED: "+identifier)
399+
}
400+
}
401+
402+
if strings.ToUpper(oc.mfa) != "AUTO" {
403+
mfaOption = findMfaOption(oc.mfa, mfaOptions, 0)
404+
} else if len(mfaOptions) > 1 {
405+
mfaOption = prompter.Choose("Select which MFA option to use", mfaOptions)
406+
}
407+
408+
challengeContext, err := getMfaChallengeContext(oc, mfaOption, resp)
409+
if err != nil {
410+
return "", err
411+
}
412+
413+
switch mfa := challengeContext.mfaIdentifer; mfa {
385414
case IdentifierYubiMfa:
386-
return gjson.Get(resp, "sessionToken").String(), nil
415+
return gjson.Get(challengeContext.challengeResponseBody, "sessionToken").String(), nil
387416
case IdentifierSmsMfa, IdentifierTotpMfa, IdentifierOktaTotpMfa, IdentifierSymantecTotpMfa:
388417
var verifyCode = loginDetails.MFAToken
389418
if verifyCode == "" {
@@ -396,7 +425,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
396425
return "", errors.Wrap(err, "error encoding token data")
397426
}
398427

399-
req, err = http.NewRequest("POST", oktaVerify, tokenBody)
428+
req, err := http.NewRequest("POST", challengeContext.oktaVerify, tokenBody)
400429
if err != nil {
401430
return "", errors.Wrap(err, "error building token post request")
402431
}
@@ -423,31 +452,26 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
423452
fmt.Printf("\nWaiting for approval, please check your Okta Verify app ...")
424453

425454
// loop until success, error, or timeout
455+
body := challengeContext.challengeResponseBody
426456
for {
427-
428-
res, err = oc.client.Do(req)
429-
if err != nil {
430-
return "", errors.Wrap(err, "error retrieving verify response")
431-
}
432-
433-
body, err = ioutil.ReadAll(res.Body)
434-
if err != nil {
435-
return "", errors.Wrap(err, "error retrieving body from response")
436-
}
437-
438457
// on 'success' status
439-
if gjson.Get(string(body), "status").String() == "SUCCESS" {
458+
if gjson.Get(body, "status").String() == "SUCCESS" {
440459
fmt.Printf(" Approved\n\n")
441-
return gjson.Get(string(body), "sessionToken").String(), nil
460+
return gjson.Get(body, "sessionToken").String(), nil
442461
}
443462

444463
// otherwise probably still waiting
445-
switch gjson.Get(string(body), "factorResult").String() {
464+
switch gjson.Get(body, "factorResult").String() {
446465

447466
case "WAITING":
448467
time.Sleep(3 * time.Second)
449468
fmt.Printf(".")
450469
logger.Debug("Waiting for user to authorize login")
470+
updatedContext, err := getMfaChallengeContext(oc, mfaOption, resp)
471+
if err != nil {
472+
return "", err
473+
}
474+
body = updatedContext.challengeResponseBody
451475

452476
case "TIMEOUT":
453477
fmt.Printf(" Timeout\n")
@@ -466,12 +490,12 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
466490
}
467491

468492
case IdentifierDuoMfa:
469-
duoHost := gjson.Get(resp, "_embedded.factor._embedded.verification.host").String()
470-
duoSignature := gjson.Get(resp, "_embedded.factor._embedded.verification.signature").String()
493+
duoHost := gjson.Get(challengeContext.challengeResponseBody, "_embedded.factor._embedded.verification.host").String()
494+
duoSignature := gjson.Get(challengeContext.challengeResponseBody, "_embedded.factor._embedded.verification.signature").String()
471495
duoSiguatres := strings.Split(duoSignature, ":")
472496
//duoSignatures[0] = TX
473497
//duoSignatures[1] = APP
474-
duoCallback := gjson.Get(resp, "_embedded.factor._embedded.verification._links.complete.href").String()
498+
duoCallback := gjson.Get(challengeContext.challengeResponseBody, "_embedded.factor._embedded.verification._links.complete.href").String()
475499

476500
// initiate duo mfa to get sid
477501
duoSubmitURL := fmt.Sprintf("https://%s/frame/web/v1/auth", duoHost)
@@ -485,7 +509,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
485509
duoForm.Add("screen_resolution_height", "1692")
486510
duoForm.Add("color_depth", "24")
487511

488-
req, err = http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode()))
512+
req, err := http.NewRequest("POST", duoSubmitURL, strings.NewReader(duoForm.Encode()))
489513
if err != nil {
490514
return "", errors.Wrap(err, "error building authentication request")
491515
}
@@ -495,7 +519,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
495519

496520
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
497521

498-
res, err = oc.client.Do(req)
522+
res, err := oc.client.Do(req)
499523
if err != nil {
500524
return "", errors.Wrap(err, "error retrieving verify response")
501525
}
@@ -560,7 +584,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
560584
return "", errors.Wrap(err, "error retrieving verify response")
561585
}
562586

563-
body, err = ioutil.ReadAll(res.Body)
587+
body, err := ioutil.ReadAll(res.Body)
564588
if err != nil {
565589
return "", errors.Wrap(err, "error retrieving body from response")
566590
}
@@ -688,7 +712,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
688712

689713
// callback to okta with cookie
690714
oktaForm := url.Values{}
691-
oktaForm.Add("id", factorID)
715+
oktaForm.Add("id", challengeContext.factorID)
692716
oktaForm.Add("stateToken", stateToken)
693717
oktaForm.Add("sig_response", fmt.Sprintf("%s:%s", duoTxCookie, duoSiguatres[1]))
694718

@@ -706,14 +730,14 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
706730

707731
// extract okta session token
708732

709-
verifyReq = VerifyRequest{StateToken: stateToken}
710-
verifyBody = new(bytes.Buffer)
733+
verifyReq := VerifyRequest{StateToken: stateToken}
734+
verifyBody := new(bytes.Buffer)
711735
err = json.NewEncoder(verifyBody).Encode(verifyReq)
712736
if err != nil {
713737
return "", errors.Wrap(err, "error encoding verify request")
714738
}
715739

716-
req, err = http.NewRequest("POST", oktaVerify, verifyBody)
740+
req, err = http.NewRequest("POST", challengeContext.oktaVerify, verifyBody)
717741
if err != nil {
718742
return "", errors.Wrap(err, "error building verify request")
719743
}
@@ -735,48 +759,83 @@ func verifyMfa(oc *Client, oktaOrgHost string, loginDetails *creds.LoginDetails,
735759
return gjson.GetBytes(body, "sessionToken").String(), nil
736760

737761
case IdentifierFIDOWebAuthn:
738-
nonce := gjson.Get(resp, "_embedded.factor._embedded.challenge.challenge").String()
739-
credentialID := gjson.Get(resp, "_embedded.factor.profile.credentialId").String()
740-
version := gjson.Get(resp, "_embedded.factor.profile.version").String()
741-
appID := oktaOrgHost
742-
webauthnCallback := gjson.Get(resp, "_links.next.href").String()
743-
744-
fidoClient, err := NewFidoClient(nonce,
745-
appID,
762+
return fidoWebAuthn(oc, oktaOrgHost, challengeContext, mfaOption, stateToken, mfaOptions, resp)
763+
}
764+
765+
// catch all
766+
return "", errors.New("no mfa options provided")
767+
}
768+
769+
func fidoWebAuthn(oc *Client, oktaOrgHost string, challengeContext *mfaChallengeContext, mfaOption int, stateToken string, mfaOptions []string, resp string) (string, error) {
770+
771+
var signedAssertion *SignedAssertion
772+
challengeResponseBody := challengeContext.challengeResponseBody
773+
lastMfaOption := mfaOption
774+
775+
for {
776+
nonce := gjson.Get(challengeResponseBody, "_embedded.factor._embedded.challenge.challenge").String()
777+
credentialID := gjson.Get(challengeResponseBody, "_embedded.factor.profile.credentialId").String()
778+
version := gjson.Get(challengeResponseBody, "_embedded.factor.profile.version").String()
779+
780+
fidoClient, err := NewFidoClient(
781+
nonce,
782+
oktaOrgHost,
746783
version,
747784
credentialID,
748785
stateToken,
749-
new(U2FDeviceFinder))
786+
new(U2FDeviceFinder),
787+
)
750788
if err != nil {
751789
return "", err
752790
}
753791

754-
signedAssertion, err := fidoClient.ChallengeU2F()
792+
signedAssertion, err = fidoClient.ChallengeU2F()
755793
if err != nil {
756-
return "", err
757-
}
794+
// if this error is not a bad key error we are done
795+
if _, ok := err.(*u2fhost.BadKeyHandleError); !ok {
796+
return "", errors.Wrap(err, "failed to perform U2F challenge")
797+
}
758798

759-
payload, err := json.Marshal(signedAssertion)
760-
if err != nil {
761-
return "", err
762-
}
763-
req, err = http.NewRequest("POST", webauthnCallback, strings.NewReader(string(payload)))
764-
if err != nil {
765-
return "", errors.Wrap(err, "error building authentication request")
766-
}
767-
req.Header.Add("Accept", "application/json")
768-
req.Header.Add("Content-Type", "application/json")
769-
res, err = oc.client.Do(req)
770-
if err != nil {
771-
return "", errors.Wrap(err, "error retrieving verify response")
772-
}
773-
body, err = ioutil.ReadAll(res.Body)
774-
if err != nil {
775-
return "", errors.Wrap(err, "error retrieving body from response")
799+
// check if there is another fido device and try that
800+
nextMfaOption := findMfaOption(oc.mfa, mfaOptions, lastMfaOption)
801+
if nextMfaOption <= lastMfaOption {
802+
return "", errors.Wrap(err, "tried all MFA options")
803+
}
804+
lastMfaOption = nextMfaOption
805+
806+
nextChallengeContext, err := getMfaChallengeContext(oc, nextMfaOption, resp)
807+
if err != nil {
808+
return "", errors.Wrap(err, "get mfa challenge failed for U2F device")
809+
}
810+
challengeResponseBody = nextChallengeContext.challengeResponseBody
811+
continue
776812
}
777-
return gjson.GetBytes(body, "sessionToken").String(), nil
813+
814+
break
778815
}
779816

780-
// catch all
781-
return "", errors.New("no mfa options provided")
817+
payload, err := json.Marshal(signedAssertion)
818+
if err != nil {
819+
return "", err
820+
}
821+
822+
webauthnCallback := gjson.Get(challengeResponseBody, "_links.next.href").String()
823+
req, err := http.NewRequest("POST", webauthnCallback, strings.NewReader(string(payload)))
824+
if err != nil {
825+
return "", errors.Wrap(err, "error building authentication request")
826+
}
827+
828+
req.Header.Add("Accept", "application/json")
829+
req.Header.Add("Content-Type", "application/json")
830+
res, err := oc.client.Do(req)
831+
if err != nil {
832+
return "", errors.Wrap(err, "error retrieving verify response")
833+
}
834+
835+
body, err := ioutil.ReadAll(res.Body)
836+
if err != nil {
837+
return "", errors.Wrap(err, "error retrieving body from response")
838+
}
839+
840+
return gjson.GetBytes(body, "sessionToken").String(), nil
782841
}

0 commit comments

Comments
 (0)