Skip to content

Commit

Permalink
Implement support for two factor logins
Browse files Browse the repository at this point in the history
Added reading TOTP token from accounts.json.
Automatic generation of OTP from token is handled by PyOTP.
For clarification, changed all previous references to "2FA" to "passwordless".
  • Loading branch information
GCMarvin authored and cal4 committed Aug 23, 2024
1 parent 92dcfb5 commit 6b9a51a
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 11 deletions.
2 changes: 2 additions & 0 deletions accounts.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
class Account:
username: str
password: str
totp: str | None = None
proxy: str | None = None
1 change: 1 addition & 0 deletions src/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 34 additions & 11 deletions src/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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...")
Expand All @@ -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
Expand Down

0 comments on commit 6b9a51a

Please sign in to comment.