def apply_credentials(self, credentials): """ Save a set of temporary credentials """ if Common.is_debug(): msg = json.dumps(obj=credentials, default=Common.json_serial, indent=4) Common.dump_out(message=msg) parser = configparser.ConfigParser() parser.read(self.profiles_location) if self.profile_name not in parser.sections(): if Common.is_debug(): Common.dump_out(message='Adding profile section {}'.format( self.profile_name)) parser.add_section(self.profile_name) creds = credentials['Credentials'] parser[self.profile_name]['AWS_ACCESS_KEY_ID'] = creds['AccessKeyId'] parser[self.profile_name]['AWS_SECRET_ACCESS_KEY'] = creds[ 'SecretAccessKey'] if 'AWS_SESSION_TOKEN' in parser[self.profile_name]: del parser[self.profile_name]['AWS_SESSION_TOKEN'] if 'SessionToken' in creds: parser[ self.profile_name]['AWS_SESSION_TOKEN'] = creds['SessionToken'] if Common.is_debug(): Common.dump_out(message='Re-writing credentials file {}'.format( self.profiles_location)) self.__write_config(path_to_file=self.profiles_location, parser=parser)
def __okta_mfa_verification(self, factor_dict, state_token, otp_value=None): """Sends the MFA token entered and retuns the response""" url = factor_dict['_links']['verify']['href'] headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Cache-Control': '"no-cache' } payload = {'stateToken': state_token} if otp_value: payload['answer'] = otp_value data = json.dumps(payload) if Common.is_debug(): Common.dump_out( "Sending MFA verification to...\nurl: {}\nbody: {}".format( url, data)) response = requests.post(url, data=data, headers=headers) if Common.is_debug(): Common.dump_out("Received {} response from Okta: {}".format( response.status_code, json.dumps(response.json()))) if response.status_code == requests.codes.ok: # pylint: disable=E1101 return response.json() else: response.raise_for_status()
def __post_auth_request(self, configuration): """ Posts a credentials-based authentication to Okta and returns an HTTP response :param configuration: clokta configuration with okta connection info :type configuration: CloktaConfiguration :return: the text from the HTTP response as a json blob :rtype: dict """ headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Cache-Control': '"no-cache', 'Authorization': 'API_TOKEN' } payload = { 'username': configuration.get('okta_username'), 'password': configuration.get('okta_password') } url = 'https://' + configuration.get('okta_org') + '/api/v1/authn' response = requests.post(url, data=json.dumps(payload), headers=headers) if Common.is_debug(): Common.dump_out( 'Requested password-based authentication with Okta.\n' + 'Response: {}'.format(response.content)) if response.status_code == requests.codes.ok: # pylint: disable=E1101 resp = json.loads(response.text) return resp else: response.raise_for_status()
def finalize_mfa(self, clokta_config, factor, otp): """ Final step in multistep process of getting a SAML token from Okta. Submit MFA response to Okta :param clokta_config: clokta configuration with mfa response :type clokta_config: CloktaConfiguration :param factor: the MFA mechanism to use :type factor: dict :param otp: the one time password for MFA :type otp: str :return: SUCCESS if succesfully authenticated. INPUT_ERROR if the otp was not correct. """ if factor['factorType'] == 'push': result = self.__do_mfa_with_push( factor=factor, state_token=self.intermediate_state_token) else: result = self.__submit_mfa_response(factor=factor, otp=otp) if result == OktaInitiator.Result.SUCCESS: if Common.is_debug(): Common.dump_out(message='Okta session token: {}'.format( self.session_token)) # Now that we have a session token, request the SAML token self.__request_saml_assertion(configuration=clokta_config, use_session_token=True) return result
def verify_preferred_factor(self): """ Return the Okta MFA configuration for the matching, supported configuration """ preferred_factors = [ opt for opt in self.option_factors if self.factor_preference == opt['prompt'] ] if preferred_factors: if Common.is_debug(): msg = 'Using preferred factor: {}'.format( self.factor_preference) Common.dump_out(message=msg) matching_okta_factor = [ fact for fact in self.okta_factors if fact['provider'] == preferred_factors[0]['provider'] and fact['factorType'] == preferred_factors[0]['factor_type'] ] return matching_okta_factor[0] else: msg = 'The MFA option \'{}\' in your configuration file is not available.\nAvailable options are {}'.format( self.factor_preference, [opt['prompt'] for opt in self.option_factors]) Common.dump_err(message=msg) raise ValueError("Unexpected MFA option") # TODO: Reprompt
def __request_saml_assertion(self, configuration, use_session_token): """ request saml 2.0 assertion :param configuration: the clokta configuration :type configuration: CloktaConfiguration :param use_session_token: whether to submit a session token with the request. The session token will be obtained from self.session_token :type use_session_token: bool :return: SUCCESS if succesfully got SAML token or INPUT_ERROR if cookie expired. Any other problem thows an exception. """ self.saml_assertion = None if use_session_token and not self.session_token: raise ValueError("No session token to use") response = self.__post_saml_request( use_session_token=use_session_token, configuration=configuration) if Common.is_debug(): Common.dump_out( message= 'Requested SAML assertion from Okta {} session token.\nResponse: {}' .format("with" if use_session_token else "without", response.content)) soup = BeautifulSoup(response.content, "html.parser") for inputtag in soup.find_all('input'): if inputtag.get('name') == 'SAMLResponse': self.saml_assertion = inputtag.get('value') if not self.saml_assertion: if not use_session_token: # If a session token is not passed in, we consider a failure as a normal possibility if Common.is_debug(): Common.dump_out('Request without session token rejected.') return OktaInitiator.Result.INPUT_ERROR else: if Common.is_debug(): Common.dump_out( 'Expecting \'<input name="SAMLResponse" value="...">\' in Okta response, but not found.' ) raise RuntimeError('Unexpected response from Okta.') else: return OktaInitiator.Result.SUCCESS
def verify_only_factor(self, factor): """ Return the Okta MFA configuration provided it is a supported configuration """ verified_factors = [ opt for opt in self.option_factors if opt['provider'] == factor['provider'] and opt['factor_type'] == factor['factorType'] ] if verified_factors: if Common.is_debug(): msg = 'Using only available factor: {}'.format( verified_factors[0]['prompt']) Common.dump_out(message=msg) return factor
def __auth_with_okta(self, configuration): """ Authenticate with Okta. If no further info is required, return SUCCESS with session_token set. If MFA response is required, return NEED_MFA with intermediate_state_token and factors set A rejection from Okta is interpretted as a bad password and INPUT_ERROR is returned. :param configuration: clokta configuration containing connection and user info :type configuration: CloktaConfiguration :return: SUCCESS, NEED_MFA or INPUT_ERROR :rtype: OktaInitiator.Result """ self.session_token = None self.factors = [] try: okta_response = self.__post_auth_request(configuration) except requests.exceptions.HTTPError as http_err: if Common.is_debug(): Common.dump_out(( 'Okta returned this credentials/password related error: {}\n' + 'This could be a mistyped password or a misconfigured username ' + 'or URL.').format(http_err)) return OktaInitiator.Result.INPUT_ERROR except Exception as err: Common.dump_err( 'Unexpected error authenticating with Okta: {}'.format(err)) raise if 'sessionToken' in okta_response and okta_response['sessionToken']: # MFA wasn't required. We've got the token. self.session_token = okta_response['sessionToken'] return OktaInitiator.Result.SUCCESS elif 'status' in okta_response and okta_response[ 'status'] == 'MFA_ENROLL': # handle case where MFA is required but no factors have been enabled Common.dump_err( 'Please enroll in multi-factor authentication before using this tool' ) raise ValueError("No MFA mechanisms configured") elif 'status' in okta_response and okta_response[ 'status'] == 'MFA_REQUIRED': self.factors = okta_response['_embedded']['factors'] if not self.factors: # Another case where no factors have been enabled raise ValueError("No MFA mechanisms configured") self.intermediate_state_token = okta_response['stateToken'] return OktaInitiator.Result.NEED_MFA else: Common.dump_err( 'Unexpected response from Okta authentication request') raise RuntimeError("Unexpected response from Okta")
def __prompt_for_role(self, with_set_default_option): """ Give the user a choice from the intersection of configured and supported factors :param with_set_default_option: if True will add an option for setting a default role :type with_set_default_option: bool :return: a tuple of what role was chosen and whether it is the new default :rtype: AwsRole, bool """ index = 1 for role in self.possible_roles: msg = '{index} - {prompt}'.format(index=index, prompt=role.role_name) Common.echo(message=msg, bold=True) index += 1 if with_set_default_option: Common.echo('{index} - set a default role'.format(index=index)) raw_choice = None try: raw_choice = click.prompt(text='Choose a Role ARN to use', type=int, err=Common.to_std_error()) choice = raw_choice - 1 except ValueError: Common.echo(message='Please select a valid option: you chose: {}'. format(raw_choice)) return self.__prompt_for_role() if choice == len(self.possible_roles): # They want to set a default. Prompt again (just without the set-default option) # and return that chosen role and that it's the new default chosen_option, _ = self.__prompt_for_role( with_set_default_option=False) return chosen_option, True if len(self.possible_roles) > choice >= 0: pass else: Common.echo(message='Please select a valid option: you chose: {}'. format(raw_choice)) return self.__prompt_for_role( with_set_default_option=with_set_default_option) chosen_option = self.possible_roles[choice] if Common.is_debug(): Common.dump_out( message='Using chosen Role {role} & IDP {idp}'.format( role=chosen_option.role_arn, idp=chosen_option.idp_arn)) return chosen_option, False
def choose_role(self): """ Look for a default role defined and, if not, prompt the user for one Allow the user to also specify the role is the default to be used from now on :return: a tuple of the chosen role and whether it is the new default :rtype: AwsRole, bool """ # throw an error if no roles are provided # (defensive coding only - this should not be possible) if not self.possible_roles: Common.dump_err( message='No AWS Role was assigned to this application!') raise ValueError( 'Unexpected configuration - No AWS role assigned to Okta login.' ) # use the one provided if there is only one if len(self.possible_roles) == 1: role = self.possible_roles[0] if self.role_preference and role.role_arn != self.role_preference: Common.dump_err( message= 'Your cofigured role "{notfound}" was not found; using "{found}" role' .format(notfound=self.role_preference, found=role.role_name)) elif Common.is_debug(): Common.echo(message="Using default role '{role}'".format( role=role.role_arn)) return role, False # use the configured role if it matches one from the the SAML assertion for role in self.possible_roles: if role.role_arn == self.role_preference: message = "Using default role '{}'".format(role.role_name) extra_message = '. Run "clokta --no-default-role" to override.' if Common.get_output_format() == Common.long_out: Common.echo(message + extra_message) else: Common.echo(message) return role, True # make the user choose return self.__prompt_for_role(with_set_default_option=True)
def __load_parameters(self, config_section): """ For each parameter this will look first in the OS environment, then in the config file (which will look in both the section and in the DEFAULT section), then in the keychain (for secrets only) and then if still not found and the attribute is required, will prompt the user. :param config_section: section of the clokta.cfg file that represents the profile that we will login to though queries on this will also look in the DEFAULT section :type config_section: :return: a map of attributes that define the clokta login, e.g. {"okta_username": "******", "multifactor_preference": "Google Authenticator", ...} :rtype: map[string, string] """ debug_msg = 'Configuration:\n' for param in self.param_list: from_env = os.getenv(key=param.name, default=-1) if from_env != -1: # If defined in environment, use that first param.value = from_env elif param.name in config_section and config_section[param.name]: # If defined in the config file, make sure it's not a secret, otherwise use it if param.secret: Common.dump_err( message= 'Invalid configuration. {} should never be defined in clokta.cfg.' .format(param.name)) raise ValueError("Illegal configuration") if param.param_type == bool: param.value = self.__validate_bool( config_section[param.name], param.name) else: param.value = config_section[param.name] elif param.secret: param.value = self.__read_from_keyring(param.name) if not param.value and param.required: # We need it. Prompt for it. param.value = self.__prompt_for(param) debug_msg += ' {}={}'.format( param.name, param.value if not param.secret else 'xxxxxxxx') if Common.is_debug(): Common.dump_out(message=debug_msg)
def choose_supported_factor(self): """ Give the user a choice from the intersection of configured and supported factors """ index = 1 for opt in self.option_factors: msg = '{index} - {prompt}'.format(index=index, prompt=opt['prompt']) Common.echo(message=msg, bold=True) index += 1 raw_choice = None try: raw_choice = click.prompt('Choose a MFA type to use', type=int, err=Common.to_std_error()) choice = raw_choice - 1 except ValueError: Common.echo(message='Please select a valid option: you chose: {}'. format(raw_choice)) return self.choose_supported_factor() if len(self.option_factors) > choice >= 0: pass else: Common.echo(message='Please select a valid option: you chose: {}'. format(raw_choice)) return self.choose_supported_factor() chosen_option = self.option_factors[choice] matching_okta_factor = [ fact for fact in self.okta_factors if fact['provider'] == chosen_option['provider'] and fact['factorType'] == chosen_option['factor_type'] ] if Common.is_debug(): Common.dump_out(message='Using chosen factor: {}'.format( chosen_option['prompt'])) return matching_okta_factor[0]
def update_configuration(self): """ Write the current version of the configuration to the clokta.cfg file """ clokta_cfg_file = configparser.ConfigParser() clokta_cfg_file.read(self.clokta_config_file) if not clokta_cfg_file.has_section(self.profile_name): clokta_cfg_file.add_section(self.profile_name) for param in self.param_list: if param.value: if param.save_to == ConfigParameter.SaveTo.DEFAULT: clokta_cfg_file.set('DEFAULT', param.name, param.value) elif param.save_to == ConfigParameter.SaveTo.PROFILE: clokta_cfg_file.set(self.profile_name, param.name, param.value) elif param.save_to == ConfigParameter.SaveTo.KEYRING: self.__save_to_keyring(param.name, param.value) if Common.is_debug(): Common.dump_out(message='Re-writing configuration file {}'.format( self.clokta_config_file)) self.__write_config(path_to_file=self.clokta_config_file, parser=clokta_cfg_file)