Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 67 additions & 69 deletions keepercommander/auth/console_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,38 @@ def __init__(self):

def on_device_approval(self, step):
if self._show_device_approval_help:
print(f"\n{Fore.YELLOW}Device Approval Required{Fore.RESET}\n")
print(f"{Fore.CYAN}Select an approval method:{Fore.RESET}")
print(f" {Fore.GREEN}1{Fore.RESET}. Email - Send approval link to your email")
print(f" {Fore.GREEN}2{Fore.RESET}. Keeper Push - Send notification to an approved device")
print(f" {Fore.GREEN}3{Fore.RESET}. 2FA Push - Send code via your 2FA method")
print()
print(f" {Fore.GREEN}c{Fore.RESET}. Enter code - Enter a verification code")
print(f" {Fore.GREEN}q{Fore.RESET}. Cancel login")
print()
logging.info(f"\n{Fore.YELLOW}Device Approval Required{Fore.RESET}\n")
logging.info(f"{Fore.CYAN}Select an approval method:{Fore.RESET}")
logging.info(f" {Fore.GREEN}1{Fore.RESET}. Email - Send approval link to your email")
logging.info(f" {Fore.GREEN}2{Fore.RESET}. Keeper Push - Send notification to an approved device")
logging.info(f" {Fore.GREEN}3{Fore.RESET}. 2FA Push - Send code via your 2FA method")
logging.info("")
logging.info(f" {Fore.GREEN}c{Fore.RESET}. Enter code - Enter a verification code")
logging.info(f" {Fore.GREEN}q{Fore.RESET}. Cancel login")
logging.info("")
self._show_device_approval_help = False
else:
print(f"\n{Fore.YELLOW}Waiting for device approval.{Fore.RESET}")
print(f"{Fore.CYAN}Check email, SMS, or push notification on the approved device.{Fore.RESET}")
print(f"Enter {Fore.GREEN}c <code>{Fore.RESET} to submit a verification code.\n")
logging.info(f"\n{Fore.YELLOW}Waiting for device approval.{Fore.RESET}")
logging.info(f"{Fore.CYAN}Check email, SMS, or push notification on the approved device.{Fore.RESET}")
logging.info(f"Enter {Fore.GREEN}c <code>{Fore.RESET} to submit a verification code.\n")

try:
selection = input(f'{Fore.GREEN}Selection{Fore.RESET} (or Enter to check status): ').strip().lower()

if selection == '1' or selection == 'email_send' or selection == 'es':
step.send_push(login_steps.DeviceApprovalChannel.Email)
print(f"\n{Fore.GREEN}Email sent to {step.username}{Fore.RESET}")
print("Click the approval link in the email, then press Enter.\n")
logging.info(f"\n{Fore.GREEN}Email sent to {step.username}{Fore.RESET}")
logging.info("Click the approval link in the email, then press Enter.\n")

elif selection == '2' or selection == 'keeper_push' or selection == 'kp':
step.send_push(login_steps.DeviceApprovalChannel.KeeperPush)
print(f"\n{Fore.GREEN}Push notification sent.{Fore.RESET}")
print("Approve on your device, then press Enter.\n")
logging.info(f"\n{Fore.GREEN}Push notification sent.{Fore.RESET}")
logging.info("Approve on your device, then press Enter.\n")

elif selection == '3' or selection == '2fa_send' or selection == '2fs':
step.send_push(login_steps.DeviceApprovalChannel.TwoFactor)
print(f"\n{Fore.GREEN}2FA code sent.{Fore.RESET}")
print("Enter the code using option 'c'.\n")
logging.info(f"\n{Fore.GREEN}2FA code sent.{Fore.RESET}")
logging.info("Enter the code using option 'c'.\n")

elif selection == 'c' or selection.startswith('c '):
# Support both "c" (prompts for code) and "c <code>" (code inline)
Expand All @@ -67,23 +67,23 @@ def on_device_approval(self, step):
# Try email code first, then 2FA
try:
step.send_code(login_steps.DeviceApprovalChannel.Email, code_input)
print(f"{Fore.GREEN}Successfully verified email code.{Fore.RESET}")
logging.info(f"{Fore.GREEN}Successfully verified email code.{Fore.RESET}")
except KeeperApiError:
try:
step.send_code(login_steps.DeviceApprovalChannel.TwoFactor, code_input)
print(f"{Fore.GREEN}Successfully verified 2FA code.{Fore.RESET}")
logging.info(f"{Fore.GREEN}Successfully verified 2FA code.{Fore.RESET}")
except KeeperApiError as e:
print(f"{Fore.YELLOW}Invalid code. Please try again.{Fore.RESET}")
logging.warning(f"{Fore.YELLOW}Invalid code. Please try again.{Fore.RESET}")

elif selection.startswith("email_code="):
code = selection.replace("email_code=", "")
step.send_code(login_steps.DeviceApprovalChannel.Email, code)
print(f"{Fore.GREEN}Successfully verified email code.{Fore.RESET}")
logging.info(f"{Fore.GREEN}Successfully verified email code.{Fore.RESET}")

elif selection.startswith("2fa_code="):
code = selection.replace("2fa_code=", "")
step.send_code(login_steps.DeviceApprovalChannel.TwoFactor, code)
print(f"{Fore.GREEN}Successfully verified 2FA code.{Fore.RESET}")
logging.info(f"{Fore.GREEN}Successfully verified 2FA code.{Fore.RESET}")

elif selection == 'q':
step.cancel()
Expand All @@ -92,14 +92,12 @@ def on_device_approval(self, step):
step.resume()

else:
print(f"{Fore.YELLOW}Invalid selection. Enter 1, 2, 3, c, q, or press Enter.{Fore.RESET}")
logging.warning(f"{Fore.YELLOW}Invalid selection. Enter 1, 2, 3, c, q, or press Enter.{Fore.RESET}")

except KeyboardInterrupt:
step.cancel()
except KeeperApiError as kae:
print()
print(f'{Fore.YELLOW}{kae.message}{Fore.RESET}')
pass
logging.warning(f'{Fore.YELLOW}{kae.message}{Fore.RESET}')

@staticmethod
def two_factor_channel_to_desc(channel): # type: (login_steps.TwoFactorChannel) -> str
Expand All @@ -122,13 +120,13 @@ def on_two_factor(self, step):
channels = step.get_channels()

if self._show_two_factor_help:
print(f"\n{Fore.YELLOW}Two-Factor Authentication Required{Fore.RESET}\n")
print(f"{Fore.CYAN}Select your 2FA method:{Fore.RESET}")
logging.info(f"\n{Fore.YELLOW}Two-Factor Authentication Required{Fore.RESET}\n")
logging.info(f"{Fore.CYAN}Select your 2FA method:{Fore.RESET}")
for i in range(len(channels)):
channel = channels[i]
print(f" {Fore.GREEN}{i+1}{Fore.RESET}. {ConsoleLoginUi.two_factor_channel_to_desc(channel.channel_type)} {channel.channel_name} {channel.phone}")
print(f" {Fore.GREEN}q{Fore.RESET}. Cancel login")
print()
logging.info(f" {Fore.GREEN}{i+1}{Fore.RESET}. {ConsoleLoginUi.two_factor_channel_to_desc(channel.channel_type)} {channel.channel_name} {channel.phone}")
logging.info(f" {Fore.GREEN}q{Fore.RESET}. Cancel login")
logging.info("")
self._show_device_approval_help = False

channel = None # type: Optional[login_steps.TwoFactorChannelInfo]
Expand All @@ -143,9 +141,9 @@ def on_two_factor(self, step):
channel = channels[idx-1]
logging.debug(f"Selected {idx}. {ConsoleLoginUi.two_factor_channel_to_desc(channel.channel_type)}")
else:
print("Invalid entry, additional factors of authentication shown may be configured if not currently enabled.")
logging.warning("Invalid entry, additional factors of authentication shown may be configured if not currently enabled.")
else:
print("Invalid entry, additional factors of authentication shown may be configured if not currently enabled.")
logging.warning("Invalid entry, additional factors of authentication shown may be configured if not currently enabled.")

mfa_prompt = False

Expand All @@ -155,9 +153,9 @@ def on_two_factor(self, step):
mfa_prompt = True
try:
step.send_push(channel.channel_uid, login_steps.TwoFactorPushAction.TextMessage)
print(f'\n{Fore.GREEN}SMS sent successfully.{Fore.RESET}\n')
logging.info(f'\n{Fore.GREEN}SMS sent successfully.{Fore.RESET}\n')
except KeeperApiError:
print("Was unable to send SMS.")
logging.warning("Was unable to send SMS.")
elif channel.channel_type == login_steps.TwoFactorChannel.SecurityKey:
try:
from ..yubikey.yubikey import yubikey_authenticate
Expand All @@ -178,14 +176,14 @@ def on_two_factor(self, step):
}
step.duration = login_steps.TwoFactorDuration.EveryLogin
step.send_code(channel.channel_uid, json.dumps(signature))
print(f'{Fore.GREEN}Security key verified.{Fore.RESET}')
logging.info(f'{Fore.GREEN}Security key verified.{Fore.RESET}')

except ImportError as e:
from ..yubikey import display_fido2_warning
display_fido2_warning()
logging.warning(e)
except KeeperApiError:
print(f'{Fore.RED}Unable to verify security key.{Fore.RESET}')
logging.error(f'{Fore.RED}Unable to verify security key.{Fore.RESET}')
except Exception as e:
logging.error(e)

Expand Down Expand Up @@ -228,7 +226,7 @@ def on_two_factor(self, step):
'Ask Every 24 hours' if mfa_expiration == login_steps.TwoFactorDuration.Every24Hours else
'Ask Every 30 days',
"|".join(allowed_expirations))
print(prompt_exp)
logging.info(prompt_exp)

try:
answer = input(f'\n{Fore.GREEN}Enter 2FA Code: {Fore.RESET}')
Expand All @@ -240,7 +238,7 @@ def on_two_factor(self, step):
if m_duration:
answer = m_duration.group(1).strip().lower()
if answer not in allowed_expirations:
print(f'Invalid 2FA Duration: {answer}')
logging.warning(f'Invalid 2FA Duration: {answer}')
answer = ''

if answer == 'login':
Expand All @@ -264,16 +262,16 @@ def on_two_factor(self, step):
step.duration = mfa_expiration
try:
step.send_code(channel.channel_uid, otp_code)
print(f'{Fore.GREEN}2FA code verified.{Fore.RESET}')
logging.info(f'{Fore.GREEN}2FA code verified.{Fore.RESET}')
except KeeperApiError:
print(f'{Fore.YELLOW}Invalid 2FA code. Please try again.{Fore.RESET}')
logging.warning(f'{Fore.YELLOW}Invalid 2FA code. Please try again.{Fore.RESET}')

def on_password(self, step):
if self._show_password_help:
logging.info(f'{Fore.CYAN}Enter master password for {Fore.WHITE}{step.username}{Fore.RESET}')

if self._failed_password_attempt > 0:
print(f'{Fore.YELLOW}Forgot password? Type "recover"<Enter>{Fore.RESET}')
logging.info(f'{Fore.YELLOW}Forgot password? Type "recover"<Enter>{Fore.RESET}')

password = getpass.getpass(prompt=f'{Fore.GREEN}Password: {Fore.RESET}', stream=None)
if not password:
Expand All @@ -284,7 +282,7 @@ def on_password(self, step):
try:
step.verify_password(password)
except KeeperApiError as kae:
print(kae.message)
logging.warning(kae.message)
except KeyboardInterrupt:
step.cancel()

Expand All @@ -300,19 +298,19 @@ def on_sso_redirect(self, step):
wb = None

sp_url = step.sso_login_url
print(f'\n{Fore.CYAN}SSO Login URL:{Fore.RESET}\n{sp_url}\n')
logging.info(f'\n{Fore.CYAN}SSO Login URL:{Fore.RESET}\n{sp_url}\n')
if self._show_sso_redirect_help:
print(f'{Fore.CYAN}Navigate to SSO Login URL with your browser and complete login.{Fore.RESET}')
print(f'{Fore.CYAN}Copy the returned SSO Token and paste it here.{Fore.RESET}')
print(f'{Fore.YELLOW}TIP: Click "Copy login token" button on the SSO Connect page.{Fore.RESET}')
print('')
print(f' {Fore.GREEN}a{Fore.RESET}. SSO User with a Master Password')
print(f' {Fore.GREEN}c{Fore.RESET}. Copy SSO Login URL to clipboard')
logging.info(f'{Fore.CYAN}Navigate to SSO Login URL with your browser and complete login.{Fore.RESET}')
logging.info(f'{Fore.CYAN}Copy the returned SSO Token and paste it here.{Fore.RESET}')
logging.info(f'{Fore.YELLOW}TIP: Click "Copy login token" button on the SSO Connect page.{Fore.RESET}')
logging.info('')
logging.info(f' {Fore.GREEN}a{Fore.RESET}. SSO User with a Master Password')
logging.info(f' {Fore.GREEN}c{Fore.RESET}. Copy SSO Login URL to clipboard')
if wb:
print(f' {Fore.GREEN}o{Fore.RESET}. Open SSO Login URL in web browser')
print(f' {Fore.GREEN}p{Fore.RESET}. Paste SSO Token from clipboard')
print(f' {Fore.GREEN}q{Fore.RESET}. Cancel SSO login')
print()
logging.info(f' {Fore.GREEN}o{Fore.RESET}. Open SSO Login URL in web browser')
logging.info(f' {Fore.GREEN}p{Fore.RESET}. Paste SSO Token from clipboard')
logging.info(f' {Fore.GREEN}q{Fore.RESET}. Cancel SSO login')
logging.info('')
self._show_sso_redirect_help = False

while True:
Expand All @@ -331,40 +329,40 @@ def on_sso_redirect(self, step):
token = None
try:
pyperclip.copy(sp_url)
print('SSO Login URL is copied to clipboard.')
logging.info('SSO Login URL is copied to clipboard.')
except:
print('Failed to copy SSO Login URL to clipboard.')
logging.warning('Failed to copy SSO Login URL to clipboard.')
elif token == 'o':
token = None
if wb:
try:
wb.open_new_tab(sp_url)
except:
print('Failed to open web browser.')
logging.warning('Failed to open web browser.')
elif token == 'p':
try:
token = pyperclip.paste()
except:
token = ''
logging.info('Failed to paste from clipboard')
logging.warning('Failed to paste from clipboard')
else:
if len(token) < 10:
print(f'Unsupported menu option: {token}')
logging.warning(f'Unsupported menu option: {token}')
token = None
if token:
step.set_sso_token(token)
break

def on_sso_data_key(self, step):
if self._show_sso_data_key_help:
print(f'\n{Fore.YELLOW}Device Approval Required for SSO{Fore.RESET}\n')
print(f'{Fore.CYAN}Select an approval method:{Fore.RESET}')
print(f' {Fore.GREEN}1{Fore.RESET}. Keeper Push - Send a push notification to your device')
print(f' {Fore.GREEN}2{Fore.RESET}. Admin Approval - Request your admin to approve this device')
print('')
print(f' {Fore.GREEN}r{Fore.RESET}. Resume SSO login after device is approved')
print(f' {Fore.GREEN}q{Fore.RESET}. Cancel SSO login')
print()
logging.info(f'\n{Fore.YELLOW}Device Approval Required for SSO{Fore.RESET}\n')
logging.info(f'{Fore.CYAN}Select an approval method:{Fore.RESET}')
logging.info(f' {Fore.GREEN}1{Fore.RESET}. Keeper Push - Send a push notification to your device')
logging.info(f' {Fore.GREEN}2{Fore.RESET}. Admin Approval - Request your admin to approve this device')
logging.info('')
logging.info(f' {Fore.GREEN}r{Fore.RESET}. Resume SSO login after device is approved')
logging.info(f' {Fore.GREEN}q{Fore.RESET}. Cancel SSO login')
logging.info('')
self._show_sso_data_key_help = False

while True:
Expand All @@ -384,4 +382,4 @@ def on_sso_data_key(self, step):
elif answer == '2':
step.request_data_key(login_steps.DataKeyShareChannel.AdminApproval)
else:
print(f'Action \"{answer}\" is not supported.')
logging.warning(f'Action \"{answer}\" is not supported.')
Loading