Example #1
0
    def __send_push(self, factor, state_token):
        ''' Send push re: Okta Verify '''
        url = factor['_links']['verify']['href']
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Cache-Control': '"no-cache'
        }
        payload = {
            'stateToken': state_token
        }

        response_data = None
        response = requests.post(url, data=json.dumps(payload), headers=headers)
        if response.status_code == requests.codes.ok:  # pylint: disable=E1101
            response_data = response.json()
        else:
            response.raise_for_status()

        Common.echo(message='Push notification sent; waiting for your response', new_line=False)

        status = response_data['status']
        if status == 'MFA_CHALLENGE':
            if 'factorResult' in response_data and response_data['factorResult'] == 'WAITING':
                return self.__check_push_result(
                    state_token=state_token,
                    push_response=response_data
                )
Example #2
0
    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()
Example #3
0
    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
Example #4
0
    def generate_configuration(cls, config_section, verbose=False):
        configuration = {}
        for field in ConfigGenerator.config_fields:
            key = field['name']
            is_secret = 'secret' in field and field['secret']
            from_env = os.getenv(key=key, default=-1)
            if from_env != -1:
                # If defined in environment, use that first
                configuration[key] = from_env
            elif key in config_section and config_section[key]:
                # If defined in the config file, make sure it's not a secret, otherwise use it
                if is_secret:
                    Common.dump_err(
                        message=
                        'Invalid configuration.  {} should never be defined in clokta.cfg.'
                        .format(key),
                        exit_code=6,
                        verbose=False)
                configuration[key] = config_section[key]
            elif 'required' in field and field['required']:
                # We need it.  Prompt for it.
                configuration[key] = ConfigGenerator.prompt_for(key)

        if verbose:
            copy_config = copy.deepcopy(configuration)
            for key in copy_config:
                if key.endswith('password'):
                    copy_config[key] = '<redacted>'
            msg = 'Configuration: {}'.format(
                json.dumps(obj=copy_config, indent=4))
            Common.dump_verbose(message=msg)

        return configuration
Example #5
0
def __okta_session_token_mfa(auth_response,
                             factors,
                             factor_preference,
                             verbose=False):
    ''' Determine which factor to use and apply it to get a session token '''
    factor = __choose_factor(factors=factors,
                             factor_preference=factor_preference,
                             verbose=verbose)
    state_token = auth_response['stateToken']

    if factor['factorType'] == 'push':
        return __send_push(factor=factor, state_token=state_token)

    if factor['factorType'] == 'sms':
        __okta_mfa_verification(factor_dict=factor,
                                state_token=state_token,
                                otp_value=None)

    otp_value = click.prompt('Enter your multifactor authentication token',
                             type=str)
    try:
        mfa_response = __okta_mfa_verification(factor_dict=factor,
                                               state_token=state_token,
                                               otp_value=otp_value)
        session_token = mfa_response['sessionToken']
    except requests.exceptions.HTTPError as http_err:
        msg = 'Okta returned this MFA related error: {}'.format(http_err)
        Common.dump_err(message=msg, exit_code=1, verbose=verbose)
    except Exception as err:
        msg = 'Unexpected error: {}'.format(err)
        Common.dump_err(message=msg, exit_code=2, verbose=verbose)

    return session_token
Example #6
0
    def __initialize_configuration(self):
        """
        Load config file, both the desired section and the default section
        :return: the parameter list
        :rtype: dict
        """
        clokta_cfg_file = configparser.ConfigParser()
        clokta_cfg_file.read(self.clokta_config_file)

        # Make sure we have the bare minimum in the config file.  The DEFAULT section and the app URL.
        if not clokta_cfg_file['DEFAULT']:
            clokta_cfg_file['DEFAULT'] = {'okta_org': ''}
        if not clokta_cfg_file.has_section(self.profile_name):
            msg = 'No profile "{}" in clokta.cfg, but enter the information and clokta will create a profile.\n' + \
                  'Copy the link from the Okta App'
            app_url = click.prompt(text=msg.format(self.profile_name),
                                   type=str,
                                   err=Common.to_std_error()).strip()
            if not app_url.startswith("https://") or not app_url.endswith(
                    "?fromHome=true"):
                Common.dump_err(
                    "Invalid App URL.  URL usually of the form https://xxxxxxxx.okta.com/.../272?fromHome=true",
                    6)
                raise ValueError("Invalid URL")
            else:
                app_url = app_url[:-len("?fromHome=true")]
            clokta_cfg_file.add_section(self.profile_name)
            clokta_cfg_file.set(self.profile_name, 'okta_aws_app_url', app_url)

        config_section = clokta_cfg_file[self.profile_name]
        self.__load_parameters(config_section)
        if self.get('save_password_in_keychain') == 'True':
            self.parameters[
                'okta_password'].save_to = ConfigParameter.SaveTo.KEYRING
Example #7
0
    def initialize_configuration(self):
        ''' Generate and load config file section '''
        parser = configparser.ConfigParser()
        parser.read(self.config_location)

        if not parser['DEFAULT']:
            parser['DEFAULT'] = {'okta_org': ''}

        if self.profile_name not in parser.sections():
            msg = 'No profile "{}" in clokta.cfg, but enter the information and clokta will create a profile.\nCopy the link from the Okta App'
            app_url = click.prompt(msg.format(self.profile_name),
                                   type=str).strip()
            if not app_url.startswith("https://") or not app_url.endswith(
                    "?fromHome=true"):
                Common.dump_err(
                    "Invalid App URL.  URL usually of the form https://xxxxxxxx.okta.com/.../272?fromHome=true",
                    6, False)
            else:
                app_url = app_url[:-len("?fromHome=true")]
            parser[self.profile_name] = {'okta_aws_app_url': app_url}

        config_section = parser[self.profile_name]
        updated_config = ConfigGenerator.generate_configuration(
            config_section=config_section, verbose=self.verbose)
        self.__write_config(path_to_file=self.config_location, parser=parser)
        return updated_config
    def generate_creds(self, role):
        """
        :param role: the AWS role the user wants to assume
        :type role: AwsRole
        """

        client = boto3.client('sts')
        # Try for a 12 hour session.  If it fails, try for shorter periods
        assumed_role_credentials = None
        durations = [43200, 14400, 3600]
        for duration in durations:
            try:
                assumed_role_credentials = client.assume_role_with_saml(
                    RoleArn=role.role_arn,
                    PrincipalArn=role.idp_arn,
                    SAMLAssertion=self.saml_assertion,
                    DurationSeconds=duration)
                if duration == 3600:
                    Common.echo(message='YOUR SESSION WILL ONLY LAST ONE HOUR')
                break
            except ClientError as e:
                # If we get a validation error and we have shorter durations to try, try a shorter duration
                if e.response['Error'][
                        'Code'] != 'ValidationError' or duration == durations[
                            -1]:
                    raise

        self.clokta_config.apply_credentials(
            credentials=assumed_role_credentials)
        self.bash_file = self.__write_sourceable_file(
            credentials=assumed_role_credentials)
        self.docker_file = self.__write_dockerenv_file(
            credentials=assumed_role_credentials)
Example #9
0
    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
Example #10
0
    def update_configuration(self, profile_configuration):
        ''' Update a config file section '''
        parser = configparser.ConfigParser()
        parser.read(self.config_location)
        
        default_keys = [
            'okta_username',
            'okta_org'
        ]
        for key in default_keys:
            parser['DEFAULT'][key] = profile_configuration.get(key)

        profile_keys = [
            'okta_aws_app_url',
            'okta_aws_role_to_assume',
            'okta_idp_provider'
        ]
        for key in profile_keys:
            parser[self.profile_name][key] = profile_configuration[key]

        if self.verbose:
            Common.dump_verbose(
                message='Re-writing configuration file {}'.format(self.config_location)
            )
        self.__write_config(
            path_to_file=self.config_location,
            parser=parser
        )
Example #11
0
    def assume_role(self, reset_default_role):
        """
        entry point for the cli tool
        :param reset_default_role: whether to reset whatever the default role is for this profile
        :type reset_default_role: bool
        """
        clokta_config_file = self.data_dir + "clokta.cfg"

        clokta_config = CloktaConfiguration(profile_name=self.profile,
                                            clokta_config_file=clokta_config_file)
        if reset_default_role:
            clokta_config.reset_default_role()

        # Attempt to initiate a connection using just cookies
        okta_initiator = OktaInitiator(data_dir=self.data_dir)
        result = okta_initiator.initiate_with_cookie(clokta_config)

        # If the cookie is expired or non-existent, INPUT_ERROR will be returned
        if result == OktaInitiator.Result.INPUT_ERROR:
            prompt_for_password = clokta_config.get('okta_password') is None
            mfas = []
            # Cookie didn't work.  Authenticate with Okta
            while result == OktaInitiator.Result.INPUT_ERROR:
                if prompt_for_password:
                    clokta_config.prompt_for(param_name='okta_password')
                result = okta_initiator.initiate_with_auth(clokta_config, mfas)
                if result == OktaInitiator.Result.INPUT_ERROR:
                    if prompt_for_password:
                        Common.dump_err("Failure.  Wrong password or misconfigured session.")
                    else:
                        Common.dump_err("Saved password may be out of date.")
                prompt_for_password = True

            if result == OktaInitiator.Result.NEED_MFA:
                done = False
                first_time = True
                while not done:
                    chosen_factor = clokta_config.determine_mfa_mechanism(mfas, force_prompt=not first_time)
                    need_otp = okta_initiator.initiate_mfa(factor=chosen_factor)
                    otp = clokta_config.determine_okta_onetimepassword(chosen_factor, first_time) if need_otp else None
                    result = okta_initiator.finalize_mfa(clokta_config=clokta_config, factor=chosen_factor, otp=otp)
                    done = result == OktaInitiator.Result.SUCCESS
                    first_time = False

        saml_assertion = okta_initiator.saml_assertion

        # We now have a SAML assertion and can generate a AWS Credentials
        aws_svc = AwsCredentialsGenerator(clokta_config=clokta_config,
                                          saml_assertion=saml_assertion,
                                          data_dir=self.data_dir)
        roles = aws_svc.get_roles()
        role = clokta_config.determine_role(roles)
        aws_svc.generate_creds(role)
        clokta_config.update_configuration()

        self.output_instructions(docker_file=aws_svc.docker_file, bash_file=aws_svc.bash_file)
Example #12
0
    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)
Example #13
0
 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 self.verbose:
             msg = 'Using only available factor: {}'.format(verified_factors[0]['prompt'])
             Common.dump_verbose(message=msg)
         return factor
Example #14
0
 def dump_account_numbers(cls, clokta_config_file):
     clokta_cfg_file = configparser.ConfigParser()
     clokta_cfg_file.read(os.path.expanduser(clokta_config_file))
     section_names = clokta_cfg_file.sections()
     for section_name in section_names:
         if clokta_cfg_file.has_option(section=section_name,
                                       option='aws_account_number'):
             acct_num = clokta_cfg_file.get(section=section_name,
                                            option='aws_account_number')
             if acct_num:
                 Common.echo("{name} = {number}".format(name=section_name,
                                                        number=acct_num))
Example #15
0
def __okta_session_token(configuration, verbose=False):
    ''' Authenticate with Okta; receive a session token '''
    okta_response = None

    try:
        okta_response = __okta_auth_response(configuration=configuration)
    except requests.exceptions.HTTPError as http_err:
        msg = 'Okta returned this credentials/password related error: {}'.format(
            http_err)
        Common.dump_err(message=msg, exit_code=1, verbose=verbose)
    except Exception as err:
        msg = 'Unexpected error: {}'.format(err)
        Common.dump_err(message=msg, exit_code=2, verbose=verbose)

    # handle case where MFA is required but no factors have been enabled
    if okta_response['status'] == 'MFA_ENROLL':
        msg = 'Please enroll in multi-factor authentication before using this tool'
        Common.dump_err(message=msg, exit_code=3, verbose=verbose)

    if okta_response['status'] == 'MFA_REQUIRED':
        factors = okta_response['_embedded']['factors']
        if factors:
            return __okta_session_token_mfa(
                auth_response=okta_response,
                factors=factors,
                factor_preference=configuration['multifactor_preference'],
                verbose=verbose)
        else:
            msg = 'No MFA factors have been set up for this account'
            Common.dump_err(message=msg, exit_code=3, verbose=verbose)

    return okta_response['sessionToken']
Example #16
0
    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)
            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 self.verbose:
            Common.dump_verbose(message='Using chosen factor: {}'.format(chosen_option['prompt']))

        return matching_okta_factor[0]
Example #17
0
    def __choose_tuple(self, idp_role_tuples):
        ''' Give the user a choice from the intersection of configured and supported factors '''
        index = 1
        for tup in idp_role_tuples:
            slashIndex = tup[2].find('/')
            shortName = tup[2][slashIndex + 1:] if slashIndex >= 0 else tup[2]
            msg = '{index} - {prompt}'.format(index=tup[0], prompt=shortName)
            Common.echo(message=msg, bold=True)
            index += 1

        raw_choice = None
        try:
            raw_choice = click.prompt('Choose a Role ARN to use', type=int)
            choice = raw_choice - 1
        except ValueError:
            Common.echo(message='Please select a valid option: you chose: {}'.
                        format(raw_choice))
            return self.__choose_tuple(idp_role_tuples=idp_role_tuples)

        if len(idp_role_tuples) > choice >= 0:
            pass
        else:
            Common.echo(message='Please select a valid option: you chose: {}'.
                        format(raw_choice))
            return self.__choose_tuple(idp_role_tuples=idp_role_tuples)

        chosen_option = idp_role_tuples[choice]
        if self.verbose:
            Common.dump_verbose(message='Using chosen Role {role} & IDP {idp}'.
                                format(role=tup[2], idp=tup[1]))

        return chosen_option
Example #18
0
    def __initialize_configuration(self):
        """
        Load config file, both the desired section and the default section
        :return: the parameter list
        :rtype: dict
        """
        clokta_cfg_file = configparser.ConfigParser()
        clokta_cfg_file.read(self.clokta_config_file)

        # Make sure we have the bare minimum in the config file.  The DEFAULT section and the app URL.
        if not clokta_cfg_file['DEFAULT']:
            clokta_cfg_file['DEFAULT'] = {}
        if not clokta_cfg_file.has_section(self.profile_name):
            msg = 'No profile "{}" in clokta.cfg, but enter the information and clokta will create a profile.\n' + \
                  'Copy the link from the Okta App'
            app_url = click.prompt(text=msg.format(self.profile_name),
                                   type=str,
                                   err=Common.to_std_error()).strip()
            app_url = self.__check_url(app_url)
            clokta_cfg_file.add_section(self.profile_name)
            clokta_cfg_file.set(self.profile_name, 'okta_aws_app_url', app_url)

        config_section = clokta_cfg_file[self.profile_name]
        self.__load_parameters(config_section)
        if self.get('save_password_in_keychain') == 'True':
            self.parameters[
                'okta_password'].save_to = ConfigParameter.SaveTo.KEYRING
Example #19
0
    def __okta_session_token(self, configuration):
        ''' Authenticate with Okta; receive a session token '''
        okta_response = None

        try:
            okta_response = self.__okta_auth_response(
                configuration=configuration)
        except requests.exceptions.HTTPError as http_err:
            if self.verbose:
                msg = 'Okta returned this credentials/password related error: {}\nThis could be a mistyped password or a misconfigured username or URL.'.format(
                    http_err)
            else:
                msg = "Failure.  Wrong password or misconfigured session."
            Common.dump_err(message=msg, exit_code=1, verbose=self.verbose)
        except Exception as err:
            msg = 'Unexpected error: {}'.format(err)
            Common.dump_err(message=msg, exit_code=2, verbose=self.verbose)

        # handle case where MFA is required but no factors have been enabled
        if okta_response['status'] == 'MFA_ENROLL':
            msg = 'Please enroll in multi-factor authentication before using this tool'
            Common.dump_err(message=msg, exit_code=3, verbose=self.verbose)

        otp_value = None
        if configuration.get('okta_onetimepassword_secret'):
            try:
                import onetimepass as otp
            except ImportError:
                msg = 'okta_onetimepassword_secret provided in config but "onetimepass" is not installed. run: pip install onetimepass'
                Common.dump_err(message=msg, exit_code=3, verbose=self.verbose)
            otp_value = otp.get_totp(
                configuration['okta_onetimepassword_secret'])

        if okta_response['status'] == 'MFA_REQUIRED':
            factors = okta_response['_embedded']['factors']
            if factors:
                return self.__okta_session_token_mfa(
                    auth_response=okta_response,
                    factors=factors,
                    factor_preference=configuration['multifactor_preference'],
                    otp_value=otp_value)
            else:
                msg = 'No MFA factors have been set up for this account'
                Common.dump_err(message=msg, exit_code=3, verbose=self.verbose)

        return okta_response['sessionToken']
Example #20
0
    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")
Example #21
0
    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)
Example #22
0
    def generate_configuration(cls, config_section, verbose=False):
        ''' Configuration file values override environment variables '''
        configuration = {
            'okta_username': '',
            'okta_password': '',
            'okta_org': '',
            'multifactor_preference': '',
            'okta_aws_app_url': '',
            'okta_aws_role_to_assume': '',
            'okta_idp_provider': ''
        }
        for key in configuration:
            if key.endswith('password'):
                configuration[key] = os.getenv(
                    key=key,
                    default=getpass.getpass(prompt="Enter a value for {}: ".format(key))
                )
            elif key == 'multifactor_preference':
                if key in config_section:
                    configuration[key] = config_section.get(key)
                else:
                    configuration[key] = ''
            else:
                if key in config_section and config_section[key] is not '':
                    configuration[key] = config_section[key]
                else:
                    configuration[key] = os.getenv(
                        key=key,
                        default=click.prompt('Enter a value for {}'.format(key), type=str)
                    )

        if verbose:
            copy_config = copy.deepcopy(configuration)
            for key in copy_config:
                if key.endswith('password'):
                    copy_config[key] = '<redacted>'
            msg = 'Configuration: {}'.format(
                json.dumps(obj=copy_config, indent=4)
            )
            Common.dump_verbose(message=msg)

        return configuration
Example #23
0
 def __validate_bool(self, value, name="parameter"):
     """
     Verify a string is either True or False.
     If it is not, print an error.
     :param value: the string value
     :type value: str
     :param name: the name or context of the value to be used in an error message
     :type name: str
     :return: "True" if the string is some form of true,  "False" for any other case
     :rtype: str
     """
     if value.strip().lower() == "true":
         return "True"
     elif value.strip().lower() == "false":
         return "False"
     else:
         Common.dump_err(
             '{} configured with value "{}" when only True or False is valid.'
             .format(name, value))
         return "False"
Example #24
0
 def __prompt_for(self, param):
     prompt = param.prompt if param.prompt else 'Enter a value for {}'.format(
         param.name)
     if param.secret:
         field_value = getpass.getpass(prompt=prompt + ":")
     else:
         field_value = click.prompt(text=prompt,
                                    type=param.param_type,
                                    err=Common.to_std_error(),
                                    default=param.default_value,
                                    show_default=not param.prompt)
     return field_value if param.param_type == str else str(field_value)
Example #25
0
    def choose_idp_role_tuple(self):
        ''' Determine the role options the user can choose from '''
        idp_role_tuples = self.__discover_role_idp_tuples()

        # throw an error if no roles are provided
        #  (defensive coding only - this should not be impossible)
        if not idp_role_tuples:
            Common.dump_err(
                message='No AWS Role was assigned to this application!',
                exit_code=4,
                verbose=self.verbose)

        # use the one prvided if there is only one
        if len(idp_role_tuples) == 1:
            role_arn = idp_role_tuples[0][2]
            if self.role_preference and role_arn != self.role_preference:
                Common.echo(
                    message='Your cofigured role was not found; using {role}'.
                    format(role=role_arn))
            else:
                Common.echo(message='Using the configured role {role}'.format(
                    role=role_arn))
            return idp_role_tuples[0]

        # use the configured role if it matches one from the the SAML assertion
        for tup in idp_role_tuples:
            if tup[2] == self.role_preference:
                return tup

        # make the user choose
        return self.__choose_tuple(idp_role_tuples=idp_role_tuples)
Example #26
0
    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()
Example #27
0
 def __read_from_keyring(self, param_name):
     """
     Read a secret from the OS keychain
     :param param_name: the name of the parameter
     :type param_name: str
     :return: the value or None if not found or otherwise could not get value
     :rtype: str
     """
     param_value = None
     if sys.version_info > (3, 0):
         system = CloktaConfiguration.KEYCHAIN_PATTERN.format(
             param_name=param_name)
         user = self.get('okta_username')
         try:
             obfuscated = keyring.get_password(system, user)
             param_value = self.__deobfuscate(obfuscated, user)
         except Exception as e:
             fail_msg = str(e)
             if fail_msg.find('Security Auth Failure') >= 0:
                 Common.dump_err(
                     'WARNING: Denied access to password in keychain.  ' +
                     'If prompted by keychain, allow access.\n' +
                     'You may need to reboot your machine before keychain will prompt again.'
                 )
             else:
                 Common.dump_err(
                     'WARNING: Could not read password from keychain: {}'.
                     format(e))
     else:
         if not self.displayed_python2_warning:
             Common.dump_err(
                 "Cannot store password in keychain.  Upgrade to Python 3 to use keychain."
             )
             self.displayed_python2_warning = True
     return param_value
Example #28
0
 def __save_to_keyring(self, param_name, param_value):
     """
     Save a secret to the keychain.  Obfuscate the secret first.
     :param param_name: the name of the parameter
     :type param_name: str
     :param param_value: the secret to save
     :type param_value: str
     """
     if sys.version_info > (3, 0):
         try:
             system = CloktaConfiguration.KEYCHAIN_PATTERN.format(
                 param_name=param_name)
             user = self.get('okta_username')
             password = self.__obfuscate(param_value, user)
             keyring.set_password(system, user, password)
         except Exception as e:
             fail_msg = str(e)
             if fail_msg.find('Security Auth Failure') >= 0:
                 Common.dump_err(fail_msg)
                 Common.dump_err(
                     'WARNING: Denied access to password in keychain.  ' +
                     'If prompted by keychain, allow access.\n' +
                     'You may need to reboot your machine before keychain will prompt again.'
                 )
             else:
                 Common.dump_err(
                     'WARNING: Could not save password to keychain: {}'.
                     format(e))
Example #29
0
    def __saml_assertion_aws(self, session_token, configuration):
        ''' fetch saml 2.0 assertion '''
        response = self.__okta_app_response(
            session_token=session_token,
            configuration=configuration
        )

        if self.verbose:
            Common.dump_verbose(
                message='SAML response: {}'.format(response.content)
            )

        soup = BeautifulSoup(response.content, "html.parser")
        assertion = None
        for inputtag in soup.find_all('input'):
            if inputtag.get('name') == 'SAMLResponse':
                assertion = inputtag.get('value')
        if not assertion:
            if self.verbose:
                Common.dump_verbose('Expecting \'<input name="SAMLResponse" value="...">\' in Okta response, but not found.')
            Common.dump_err(
                message='Unexpected response from Okta.',
                exit_code=4,
                verbose=self.verbose
            )
        return assertion
Example #30
0
    def __check_push_result(self, state_token, push_response):
        ''' Wait for push response acknowledgement '''
        url = push_response['_links']['next']['href']
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Cache-Control': '"no-cache'
        }
        payload = {
            'stateToken': state_token
        }

        wait_for = 60
        timeout = time.time() + wait_for
        response_data = None
        while True:
            Common.echo(message='.', new_line=False)
            response = requests.post(url, data=json.dumps(payload), headers=headers)
            if response.status_code == requests.codes.ok:  # pylint: disable=E1101
                response_data = response.json()
            else:
                response.raise_for_status()

            if 'sessionToken' in response_data or time.time() > timeout:
                Common.echo(message='Session confirmed')
                break
            time.sleep(3)

        if response_data:
            return response_data['sessionToken']
        else:
            msg = 'Timeout expired ({} seconds)'.format(wait_for)
            Common.dump_err(message=msg, exit_code=3)