diff --git a/README.md b/README.md index c4f75d7..6b8b878 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ A configuration wizard will prompt you to enter the necessary configuration para - aws_default_duration = This is optional. Lifetime for temporary credentials, in seconds. Defaults to 1 hour (3600) - app_url - If using 'appurl' setting for gimme_creds_server, this sets the url to the aws application configured in Okta. It is typically something like - okta_username - use this username to authenticate +- enable_keychain - enable the use of the system keychain to store the user's password - preferred_mfa_type - automatically select a particular device when prompted for MFA: - push - Okta Verify App push or DUO push (depends on okta supplied provider type) - token:software:totp - OTP using the Okta Verify App diff --git a/gimme_aws_creds/config.py b/gimme_aws_creds/config.py index a7f2bfd..6dc8bbf 100644 --- a/gimme_aws_creds/config.py +++ b/gimme_aws_creds/config.py @@ -37,6 +37,7 @@ def __init__(self, gac_ui, create_config=True): 'OKTA_CONFIG', os.path.join(self.FILE_ROOT, '.okta_aws_login_config') ) + self.disable_keychain = False self.open_browser = False self.action_register_device = False self.username = None @@ -152,6 +153,10 @@ def get_args(self): '--open-browser', action='store_true', help='Automatically open a webbrowser for device authorization (Okta Identity Engine only)' ) + parser.add_argument( + '--disable-keychain', action='store_true', + help="Disable the use of the system keychain to store the user's password" + ) parser.add_argument( '--force-classic', action='store_true', help='Force the use of the Okta Classic login process (Okta Identity Engine only)' @@ -165,6 +170,7 @@ def get_args(self): self.action_register_device = args.action_register_device self.action_setup_fido_authenticator = args.action_setup_fido_authenticator self.open_browser = args.open_browser + self.disable_keychain = args.disable_keychain self.force_classic = args.force_classic if args.insecure is True: @@ -189,6 +195,13 @@ def get_args(self): self.conf_profile = args.profile or 'DEFAULT' def _handle_config(self, config, profile_config, include_inherits = True): + # Convert True/False strings to booleans + for key in profile_config: + if profile_config[key] == 'True': + profile_config[key] = True + elif profile_config[key] == 'False': + profile_config[key] = False + if "inherits" in profile_config.keys() and include_inherits: self.ui.message("Using inherited config: " + profile_config["inherits"]) if profile_config["inherits"] not in config: @@ -238,6 +251,7 @@ def update_config_file(self): aws_default_duration = Default AWS session duration (3600) preferred_mfa_type = Select this MFA device type automatically include_path - (optional) includes that full role path to the role name for profile + enable_keychain = (optional) enable the use of the system keychain to store the user's password """ config = configparser.ConfigParser() @@ -262,7 +276,8 @@ def update_config_file(self): 'aws_default_duration': '3600', 'output_format': 'export', 'force_classic': '', - 'open_browser': '' + 'open_browser': '', + 'enable_keychain': 'y' } # See if a config file already exists. @@ -292,6 +307,7 @@ def update_config_file(self): # These options are only used in the Classic authentication flow if self._okta_platform == 'classic' or config_dict['force_classic'] is True: config_dict['okta_username'] = self._get_okta_username(defaults['okta_username']) + config_dict['enable_keychain'] = self._get_enable_keychain(defaults['enable_keychain']) config_dict['preferred_mfa_type'] = self._get_preferred_mfa_type(defaults['preferred_mfa_type']) config_dict['remember_device'] = self._get_remember_device(defaults['remember_device']) @@ -387,6 +403,15 @@ def _get_auth_server_entry(self, default_entry): self._okta_auth_server = okta_auth_server return okta_auth_server + + def _get_enable_keychain(self, default_entry): + """ enable the use of the system keychain to store the user's password """ + + while True: + try: + return self._get_user_input_yes_no("Use the system keychain to store the user's password? (y/n)", default_entry) + except ValueError: + ui.default.warning("Enable keychain must be either y or n.") def _get_client_id_entry(self, default_entry): """ Get and validate client_id """ diff --git a/gimme_aws_creds/main.py b/gimme_aws_creds/main.py index 553ebd1..a5a1577 100644 --- a/gimme_aws_creds/main.py +++ b/gimme_aws_creds/main.py @@ -460,6 +460,9 @@ def generate_config(self): config.get_args() self._cache['conf_dict'] = config.get_config_dict() + if config.disable_keychain is True: + self.conf_dict['enable_keychain'] = False + for value in self.envvar_list: if self.ui.environ.get(value): key = self.envvar_conf_map.get(value, value).lower() @@ -566,6 +569,7 @@ def okta(self): self.okta_org_url, self.config.verify_ssl_certs, self.device_token, + self.conf_dict.get('enable_keychain', True) ) if self.config.username is not None: diff --git a/gimme_aws_creds/okta_classic.py b/gimme_aws_creds/okta_classic.py index fcd090a..0c93c7a 100644 --- a/gimme_aws_creds/okta_classic.py +++ b/gimme_aws_creds/okta_classic.py @@ -45,7 +45,7 @@ class OktaClassicClient(object): KEYRING_SERVICE = 'gimme-aws-creds' KEYRING_ENABLED = not isinstance(keyring.get_keyring(), FailKeyring) - def __init__(self, gac_ui, okta_org_url, verify_ssl_certs=True, device_token=None): + def __init__(self, gac_ui, okta_org_url, verify_ssl_certs=True, device_token=None, use_keyring=True): """ :type gac_ui: ui.UserInterface :param okta_org_url: Base URL string for Okta IDP. @@ -56,6 +56,8 @@ def __init__(self, gac_ui, okta_org_url, verify_ssl_certs=True, device_token=Non self._okta_org_url = okta_org_url self._verify_ssl_certs = verify_ssl_certs + self._use_keyring = use_keyring + if verify_ssl_certs is False: requests.packages.urllib3.disable_warnings() @@ -357,7 +359,7 @@ def _login_username_password(self, state_token, url): # ref: https://developer.okta.com/docs/reference/error-codes/#example-errors-listed-by-http-return-code elif response.status_code in [400, 401, 403, 404, 409, 429, 500, 501, 503]: if response_data['errorCode'] == "E0000004": - if self.KEYRING_ENABLED: + if self.KEYRING_ENABLED and self._use_keyring: try: self.ui.info("Stored password is invalid, clearing. Please try again") keyring.delete_password(self.KEYRING_SERVICE, creds['username']) @@ -901,7 +903,7 @@ def _get_username_password_creds(self): username = self._username password = self._password - if not password and self.KEYRING_ENABLED: + if not password and self.KEYRING_ENABLED and self._use_keyring: try: # If the OS supports a keyring, offer to save the password password = keyring.get_password(self.KEYRING_SERVICE, username) @@ -917,7 +919,7 @@ def _get_username_password_creds(self): if len(password) > 0: break - if self.KEYRING_ENABLED: + if self.KEYRING_ENABLED and self._use_keyring: # If the OS supports a keyring, offer to save the password if self.ui.input("Do you want to save this password in the keyring? (y/N) ").lower() == 'y': try: diff --git a/tests/test_config.py b/tests/test_config.py index caf0a75..a100201 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -38,7 +38,8 @@ def tearDown(self): action_store_json_creds=False, action_setup_fido_authenticator=False, open_browser=False, - force_classic=False + force_classic=False, + disable_keychain=False ), ) def test_get_args_username(self, mock_arg):