def authenticate_to_adfs_portal(self, response): payload = self.generate_payload_from_login_page(response) idp_auth_form_submit_url = self.build_idp_auth_url(response) logger.debug( 'Posting login data to URL: {}'.format(idp_auth_form_submit_url)) login_response = self.session.post(idp_auth_form_submit_url, data=payload, verify=True) login_response_page = BeautifulSoup(login_response.text, "html.parser") # Checks for errorText id on page to indicate any errors login_error_message = login_response_page.find(id='errorText') # Checks for specific text in a paragraph element to indicate any errors vip_login_error_message = login_response_page.find( lambda tag: tag.name == "p" and "Authentication failed" in tag.text ) if (login_error_message and len(login_error_message.string) > 0) or ( vip_login_error_message and len(vip_login_error_message) > 0): msg = ('Login page returned the following message. ' 'Please resolve this issue before continuing:') click.secho(msg, fg='red') error_msg = login_error_message if login_error_message else vip_login_error_message click.secho(error_msg.string, fg='red') sys.exit(1) return login_response
def handle_okta_verification(self, response): # If there is no assertion, and we find an Okta portal on the page, # we have to pass through the Okta portal regardless of whether MFA # will be required or not. if not self.okta_org: msg = ('Okta MFA required but no Okta Organization set. ' 'Please either set in the config or use `--okta-org`') click.secho(msg, fg='red') sys.exit(1) verification_status = self.get_verification_status_from_response( response) logger.debug( 'Current Verification Status: {}.'.format(verification_status)) if verification_status == 'success': logger.debug( 'Okta portal already authenticated, passing through...') # If the Okta portal status is already 'success', we can just # pass through the Okta portal, otherwise, we will have to do MFA. okta_form_submit_response = self.submit_adapter_glue_form(response) return okta_form_submit_response elif verification_status == 'mfa_required': # If the Okta portal is in 'mfa_required' status, # we need to begin the MFA process. okta_available_factors = self.fetch_available_mfa_factors() mfa_verified = self.process_okta_mfa(okta_available_factors) if mfa_verified: okta_response = self.submit_adapter_glue_form(response) return okta_response click.secho('Okta verification failed. Exiting...', fg='red') sys.exit(1)
def okta_totp_verification(self, factor_details): if not self.okta_shared_secret: logger.debug( 'TOTP Verification available but Okta Shared Secret ' 'is not set. For instructions to set the Shared Secret, ' 'refer to the README: ' 'https://github.com/cshamrick/stsauth/blob/master/README.md') return False totp = pyotp.TOTP(self.okta_shared_secret) verify_url = factor_details.get('_links', {}).get('verify', {}).get('href') try: pass_code = totp.now() except Exception as e: msg = 'An error occured fetching your TOTP code. Please check your Shared Secret.' click.secho(msg, fg='red') click.secho('Error: {}'.format(str(e)), fg='red') sys.exit(1) if verify_url: verify_response = self.session.post(verify_url, json={ 'stateToken': self.state_token, 'passCode': pass_code }) if verify_response.ok: status = verify_response.json().get('status') if status == 'SUCCESS': return True click.secho( 'TOTP Verification failed. ' 'Continuing to other methods if available', fg='red') return False
def submit_adapter_glue_form(self, response): response.soup = BeautifulSoup(response.content, 'lxml') adapter_glue_form = response.soup.find(id='adapterGlue') referer = response.url self.session.headers.update({'Referer': referer}) selectors = ",".join("{}[name]".format(i) for i in ("input", "button", "textarea", "select")) data = [(tag.get('name'), tag.get('value')) for tag in adapter_glue_form.select(selectors)] logger.debug('Posting data to url: {}\n{}'.format(referer, data)) return self.session.post(referer, data=data)
def get_saml_response(self, response=None): if not response: logger.debug('No response provided. Fetching IDP Entry URL...') response = self.session.get(self.idpentryurl) response.soup = BeautifulSoup(response.text, "lxml") assertion_pattern = re.compile( r'name=\"SAMLResponse\" value=\"(.*)\"\s*/><noscript>') assertion = re.search(assertion_pattern, response.text) if assertion: # If there is already an assertion in the response body, # we can attach the parsed assertion to the response object and # return the whole response for use later. # return account_map, assertion.group(1) response.assertion = assertion.group(1) return response logger.debug( 'No SAML assertion found in response. Attempting to log in...') login_form = response.soup.find(id='loginForm') okta_login = response.soup.find(id='okta-login-container') if okta_login: state_token = utils.get_state_token_from_response(response.text) if state_token is None: click.secho('No State Token found in response. Exiting...', fg='red') sys.exit(1) okta_client = Okta(session=self.session, state_token=state_token, okta_org=self.okta_org, okta_shared_secret=self.okta_shared_secret) okta_response = okta_client.handle_okta_verification(response) return self.get_saml_response(response=okta_response) if login_form: # If there is no assertion, it is possible the user is attempting # to authenticate from outside the network, so we check for a login # form in their response. form_response = self.authenticate_to_adfs_portal(response) return self.get_saml_response(response=form_response) else: msg = 'Response did not contain a valid SAML assertion, a valid login form, or request MFA.' click.secho(msg, fg='red') sys.exit(1)
def authenticate_to_adfs_portal(self, response): payload = self.generate_payload_from_login_page(response) idp_auth_form_submit_url = self.build_idp_auth_url(response) logger.debug( 'Posting login data to URL: {}'.format(idp_auth_form_submit_url)) login_response = self.session.post(idp_auth_form_submit_url, data=payload, verify=True) login_response_page = BeautifulSoup(login_response.text, "html.parser") login_error_message = login_response_page.find(id='errorText') if login_error_message and len(login_error_message.string) > 0: msg = ('Login page returned the following message. ' 'Please resolve this issue before continuing:') click.secho(msg, fg='red') click.secho(login_error_message.string, fg='red') sys.exit(1) return login_response
def generate_payload_from_login_page(self, response): login_page = BeautifulSoup(response.text, "html.parser") payload = {} for input_tag in login_page.find_all(re.compile('(INPUT|input)')): name = input_tag.get('name', '') value = input_tag.get('value', '') logger.debug( 'Adding value for {!r} to Login Form payload.'.format(name)) if "user" in name.lower(): payload[name] = self.domain_user elif "email" in name.lower(): payload[name] = self.domain_user elif "pass" in name.lower(): payload[name] = self.password elif "security_code" in name.lower(): payload[name] = self.vip_access_security_code else: payload[name] = value return payload
def okta_push_verification(self, factor_details, notify_count=5, poll_count=10): status = 'MFA_CHALLENGE' tries = 0 verify_data = {'stateToken': self.state_token} verify_url = factor_details.get('_links', {}).get('verify', {}).get('href') if verify_url is None: click.secho( 'No Okta verification URL present in response. Exiting...', fg='red') sys.exit(1) while (status == 'MFA_CHALLENGE' and tries < notify_count): msg = '({}/{}) Waiting for Okta push notification to be accepted...' click.secho(msg.format(tries + 1, notify_count), fg='green') for _ in range(poll_count): verify_response = self.session.post(verify_url, json=verify_data) if verify_response.ok: verify_response_json = verify_response.json() logger.debug('Okta Verification Response:\n{}'.format( verify_response_json)) status = verify_response_json.get('status', 'MFA_CHALLENGE') if verify_response_json.get('factorResult') == 'REJECTED': click.secho( 'Okta push notification was rejected! Exiting...', fg='red') sys.exit(1) if status == 'SUCCESS': break time.sleep(1) tries += 1 return status == 'SUCCESS'
def process_okta_mfa(self, okta_available_factors): # The full list of available factor types is available here: # https://developer.okta.com/docs/api/resources/factors#factor-type # ['token:software:totp', 'push', 'sms', 'question', 'call', 'token', 'token:hardware', 'web'] logger.debug('Available Okta MFA factors found: {}.'.format(', '.join( okta_available_factors.keys()))) if 'token:software:totp' in okta_available_factors.keys(): logger.debug( 'Okta TOTP Verification Method available, attempting to verify...' ) totp_factor = okta_available_factors.get('token:software:totp') if self.okta_totp_verification(totp_factor): return True if 'push' in okta_available_factors.keys(): logger.debug( 'Okta Push Verification Method available, attempting to verify...' ) push_factor = okta_available_factors.get('push') if self.okta_push_verification(push_factor): return True return False
def parse_config_file(self): """Read configuration file and only set values if they were not passed in from the CLI. """ if self.config.has_section('default'): logger.debug('Found \'default\' section in' ' {0.credentialsfile!r}!'.format(self)) default = self.config['default'] msg = ( 'Attribute {1!r} not set, using value from {0.credentialsfile!r}' ) if not self.region: logger.debug(msg.format(self, 'region')) self.region = default.get('region') if not self.output: logger.debug(msg.format(self, 'output')) self.output = default.get('output') if not self.idpentryurl: logger.debug(msg.format(self, 'idpentryurl')) self.idpentryurl = default.get('idpentryurl') if not self.domain: logger.debug(msg.format(self, 'domain')) self.domain = default.get('domain') if not self.okta_org: logger.debug(msg.format(self, 'okta_org')) self.okta_org = default.get('okta_org') if not self.okta_shared_secret: logger.debug(msg.format(self, 'okta_shared_secret')) self.okta_shared_secret = default.get('okta_shared_secret') else: logger.debug('Could not find \'default\' section in' ' {0.credentialsfile!r}!'.format(self))