@@ -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
7988func 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 ("\n Waiting 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