2323import java .util .LinkedList ;
2424import java .util .List ;
2525import java .util .Queue ;
26+ import org .apache .commons .lang3 .StringUtils ;
2627import org .apache .logging .log4j .LogManager ;
2728import org .apache .logging .log4j .Logger ;
2829import org .openqa .selenium .By ;
3334import org .openqa .selenium .support .ui .ExpectedConditions ;
3435import org .openqa .selenium .support .ui .WebDriverWait ;
3536import org .parosproxy .paros .Constant ;
37+ import org .parosproxy .paros .network .HttpMessage ;
3638import org .zaproxy .addon .authhelper .AuthUtils ;
3739import org .zaproxy .addon .authhelper .AuthenticationDiagnostics ;
3840import org .zaproxy .addon .authhelper .internal .AuthenticationStep ;
41+ import org .zaproxy .addon .commonlib .internal .TotpSupport ;
3942import org .zaproxy .zap .authentication .UsernamePasswordAuthenticationCredentials ;
4043import org .zaproxy .zap .model .Context ;
4144
4245public final class MsLoginAuthenticator implements Authenticator {
4346
4447 private static final String PARTIAL_LOGIN_URL = "login.microsoftonline" ;
4548
49+ private static final String LOGIN_URL = "https://" + PARTIAL_LOGIN_URL ;
50+
4651 private static final Logger LOGGER = LogManager .getLogger (MsLoginAuthenticator .class );
4752
4853 private static final Duration PAGE_LOAD_WAIT_UNTIL = Duration .ofSeconds (5 );
@@ -53,6 +58,8 @@ public final class MsLoginAuthenticator implements Authenticator {
5358 private static final By SUBMIT_BUTTON = By .id ("idSIButton9" );
5459 private static final By KMSI_FIELD = By .id ("KmsiCheckboxField" );
5560 private static final By PROOF_REDIRECT_FIELD = By .id ("idSubmit_ProofUp_Redirect" );
61+ private static final By PROOF_TOTP_FIELD = By .id ("idTxtBx_SAOTCC_OTC" );
62+ private static final By PROOF_TOTP_VERIFY_FIELD = By .id ("idSubmit_SAOTCC_Continue" );
5663 private static final By PROOF_DONE_FIELD = By .id ("id__5" );
5764
5865 private enum State {
@@ -68,9 +75,15 @@ private enum State {
6875 STAY_SIGNED_IN ,
6976
7077 PROOF_REDIRECT ,
78+ PROOF_TOTP ,
7179 PROOF ,
7280 }
7381
82+ @ Override
83+ public boolean isOwnSite (HttpMessage msg ) {
84+ return msg .getRequestHeader ().getURI ().toString ().startsWith (LOGIN_URL );
85+ }
86+
7487 @ Override
7588 public Result authenticate (
7689 AuthenticationDiagnostics diags ,
@@ -101,6 +114,7 @@ private Result authenticateImpl(
101114 boolean successful = false ;
102115 boolean userField = false ;
103116 boolean pwdField = false ;
117+ int totpRetries = 0 ;
104118
105119 do {
106120 switch (states .remove ()) {
@@ -117,13 +131,16 @@ private Result authenticateImpl(
117131
118132 if (findElement (wd , By .tagName ("div" ), "Pick an account" ) != null ) {
119133 states .add (State .ACCOUNT_SELECTION );
134+ } else if (findElement (wd , PROOF_TOTP_FIELD ) != null ) {
135+ userField = true ;
136+ pwdField = true ;
137+ states .add (State .PROOF_TOTP );
120138 } else {
121139 diags .recordStep (
122140 wd ,
123141 Constant .messages .getString (
124142 "authhelper.auth.method.diags.steps.ms.missingusername" ));
125- LOGGER .debug (
126- "Expected username field not found nor pick an account, failing login." );
143+ LOGGER .debug ("Unexpected initial state, failing login." );
127144 }
128145
129146 break ;
@@ -246,6 +263,21 @@ private Result authenticateImpl(
246263 // Ignore, there's still the next step to check.
247264 }
248265
266+ try {
267+ WebElement proofTotpElement =
268+ waitForElement (
269+ wd ,
270+ new ElemenContainsText (
271+ By .id ("idDiv_SAOTCC_Description" ),
272+ "authenticator" ));
273+ if (proofTotpElement != null ) {
274+ states .add (State .PROOF_TOTP );
275+ break ;
276+ }
277+ } catch (TimeoutException e ) {
278+ // Ignore, there's still the next step to check.
279+ }
280+
249281 try {
250282 waitForElement (wd , KMSI_FIELD );
251283 states .add (State .STAY_SIGNED_IN );
@@ -286,6 +318,38 @@ private Result authenticateImpl(
286318 states .add (State .PROOF );
287319 break ;
288320
321+ case PROOF_TOTP :
322+ totpRetries ++;
323+
324+ if (totpRetries > 2 ) {
325+ diags .recordStep (
326+ wd ,
327+ Constant .messages .getString (
328+ "authhelper.auth.method.diags.steps.ms.clickprooftotperror" ));
329+ LOGGER .debug ("TOTP proof failed, assuming unsuccessful login." );
330+ break ;
331+ }
332+
333+ WebElement proofTotpElement = wd .findElement (PROOF_TOTP_FIELD );
334+ WebElement proofTotpVerifyElement = wd .findElement (PROOF_TOTP_VERIFY_FIELD );
335+ proofTotpElement .clear ();
336+ proofTotpElement .sendKeys (TotpSupport .getCode (credentials ));
337+ proofTotpVerifyElement .click ();
338+ diags .recordStep (
339+ wd ,
340+ Constant .messages .getString (
341+ "authhelper.auth.method.diags.steps.ms.clickprooftotpverify" ),
342+ proofTotpVerifyElement );
343+
344+ if (findElementContains (wd , By .id ("idDiv_SAOTCC_ErrorMsg_OTC" ), "try again" )
345+ != null ) {
346+ states .add (State .PROOF_TOTP );
347+ } else {
348+ states .add (State .POST_PASSWORD );
349+ }
350+
351+ break ;
352+
289353 case PROOF :
290354 try {
291355 waitForElement (wd , new ElementWithText (By .tagName ("button" ), "Skip setup" ));
@@ -320,6 +384,17 @@ private static boolean isUserLoggedIn(WebDriver wd, WebElement userElement) {
320384 .anyMatch (e -> "Signed in" .equalsIgnoreCase (e .getText ()));
321385 }
322386
387+ private static WebElement findElement (WebDriver wd , By by ) {
388+ return wd .findElements (by ).stream ().findFirst ().orElse (null );
389+ }
390+
391+ private static WebElement findElementContains (WebDriver wd , By by , String text ) {
392+ return wd .findElements (by ).stream ()
393+ .filter (e -> StringUtils .containsIgnoreCase (e .getText (), text ))
394+ .findFirst ()
395+ .orElse (null );
396+ }
397+
323398 private static WebElement findElement (WebDriver wd , By by , String text ) {
324399 return wd .findElements (by ).stream ()
325400 .filter (e -> text .equalsIgnoreCase (e .getText ()))
@@ -372,4 +447,28 @@ public String toString() {
372447 return String .format ("element '%s' with text '%s' is not present" , locator , text );
373448 }
374449 }
450+
451+ private static class ElemenContainsText implements ExpectedCondition <WebElement > {
452+
453+ private final By locator ;
454+ private final String text ;
455+
456+ ElemenContainsText (By locator , String text ) {
457+ this .locator = locator ;
458+ this .text = text ;
459+ }
460+
461+ @ Override
462+ public WebElement apply (WebDriver driver ) {
463+ return driver .findElements (locator ).stream ()
464+ .filter (e -> StringUtils .containsIgnoreCase (e .getText (), text ))
465+ .findFirst ()
466+ .orElse (null );
467+ }
468+
469+ @ Override
470+ public String toString () {
471+ return String .format ("element '%s' containing text '%s' is not present" , locator , text );
472+ }
473+ }
375474}
0 commit comments