diff --git a/accounts.json.sample b/accounts.json.sample index b341c57f..9aceda60 100644 --- a/accounts.json.sample +++ b/accounts.json.sample @@ -2,11 +2,13 @@ { "username": "Your Email 1", "password": "Your Password 1", + "totp": "0123 4567 89ab cdef", "proxy": "http://user:pass@host1:port" }, { "username": "Your Email 2", "password": "Your Password 2", + "totp": "0123 4567 89ab cdef", "proxy": "http://user:pass@host2:port" } ] diff --git a/requirements.txt b/requirements.txt index 074533cf..f22b270a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ pyyaml~=6.0.2 urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability requests-oauthlib zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability +pyotp diff --git a/src/account.py b/src/account.py index 35773dd6..3ec30515 100644 --- a/src/account.py +++ b/src/account.py @@ -5,4 +5,5 @@ class Account: username: str password: str + totp: str | None = None proxy: str | None = None diff --git a/src/browser.py b/src/browser.py index ad95b509..9cee35a9 100644 --- a/src/browser.py +++ b/src/browser.py @@ -40,6 +40,7 @@ def __init__( self.headless = not args.visible self.username = account.username self.password = account.password + self.totp = account.totp self.localeLang, self.localeGeo = self.getCCodeLang(args.lang, args.geo) self.proxy = None if args.proxy: diff --git a/src/login.py b/src/login.py index 93426c65..756d853c 100644 --- a/src/login.py +++ b/src/login.py @@ -3,6 +3,7 @@ import logging from argparse import Namespace +from pyotp import TOTP from selenium.common import TimeoutException from selenium.webdriver.common.by import By from undetected_chromedriver import Chrome @@ -42,13 +43,13 @@ def executeLogin(self) -> None: self.utils.waitUntilClickable(By.ID, "idSIButton9").click() # noinspection PyUnusedLocal - isTwoFactorEnabled: bool = False + isPasswordlessEnabled: bool = False with contextlib.suppress(TimeoutException): self.utils.waitUntilVisible(By.ID, "pushNotificationsTitle") - isTwoFactorEnabled = True - logging.debug(f"isTwoFactorEnabled = {isTwoFactorEnabled}") + isPasswordlessEnabled = True + logging.debug(f"isPasswordlessEnabled = {isPasswordlessEnabled}") - if isTwoFactorEnabled: + if isPasswordlessEnabled: # todo - Handle 2FA when running headless assert ( self.args.visible @@ -58,13 +59,6 @@ def executeLogin(self) -> None: ) input() - with contextlib.suppress( - TimeoutException - ): # In case user clicked stay signed in - self.utils.waitUntilVisible( - By.NAME, "kmsiForm" - ) # kmsi = keep me signed form - self.utils.waitUntilClickable(By.ID, "acceptButton").click() else: passwordField = self.utils.waitUntilClickable(By.NAME, "passwd") logging.info("[LOGIN] Entering password...") @@ -73,6 +67,35 @@ def executeLogin(self) -> None: assert passwordField.get_attribute("value") == self.browser.password self.utils.waitUntilClickable(By.ID, "idSIButton9").click() + # noinspection PyUnusedLocal + isTwoFactorEnabled: bool = False + with contextlib.suppress(TimeoutException): + self.utils.waitUntilVisible(By.ID, "idTxtBx_SAOTCC_OTC") + isTwoFactorEnabled = True + logging.debug(f"isTwoFactorEnabled = {isTwoFactorEnabled}") + + if isTwoFactorEnabled: + if self.browser.totp is not None: + # TOTP token provided + logging.info("[LOGIN] Entering OTP...") + otp = TOTP(self.browser.totp.replace(" ", "")).now() + otpField = self.utils.waitUntilClickable(By.ID, "idTxtBx_SAOTCC_OTC") + otpField.send_keys(otp) + assert otpField.get_attribute("value") == otp + self.utils.waitUntilClickable(By.ID, "idSubmit_SAOTCC_Continue").click() + else: + # No TOTP token provided, manual intervention required + assert ( + self.args.visible + ), "2FA detected, provide token in accounts.json or run in visible mode to handle login" + print( + "2FA detected, handle prompts and press enter when on keep me signed in page" + ) + input() + + with contextlib.suppress( + TimeoutException + ): # In case user clicked stay signed in self.utils.waitUntilVisible( By.NAME, "kmsiForm" ) # kmsi = keep me signed form