def test_prepare_payload(): """Check if values passed return in a dictionary.""" from tokendito import helpers assert helpers.prepare_payload(pytest_key="pytest_val") == { "pytest_key": "pytest_val" } assert helpers.prepare_payload(pytest_key=None) == {"pytest_key": None} assert helpers.prepare_payload( pytest_key1="pytest_val1", pytest_key2="pytest_val2" ) == {"pytest_key1": "pytest_val1", "pytest_key2": "pytest_val2"}
def test_prepare_payload(): """Check if values passed return in a dictionary.""" from tokendito import helpers assert helpers.prepare_payload(pytest_key='pytest_val') == { 'pytest_key': 'pytest_val' } assert helpers.prepare_payload(pytest_key=None) == {'pytest_key': None} assert helpers.prepare_payload(pytest_key1='pytest_val1', pytest_key2='pytest_val2') == { 'pytest_key1': 'pytest_val1', 'pytest_key2': 'pytest_val2' }
def authenticate_user(okta_url, okta_username, okta_password): """Authenticate user with okta credential. :param okta_url: company specific URL of the okta :param okta_username: okta username :param okta_password: okta password :return: MFA session options """ logging.debug("Authenticate user with okta credential [{} user {}]".format( okta_url, okta_username)) headers = { "content-type": "application/json", "accept": "application/json" } payload = helpers.prepare_payload(username=okta_username, password=okta_password) primary_auth = okta_verify_api_method("{}/api/v1/authn".format(okta_url), payload, headers) logging.debug("Authenticate Okta header [{}] ".format(headers)) session_token = user_mfa_challenge(headers, primary_auth) logging.info("User has been succesfully authenticated.") return session_token
def user_mfa_options(selected_mfa_option, headers, mfa_challenge_url, payload, primary_auth): """Handle user mfa options. :param selected_mfa_option: Selected MFA option (SMS, push, etc) :param headers: headers :param mfa_challenge_url: MFA challenge URL :param payload: payload :param primary_auth: Primary authentication method :return: payload data """ logging.debug("Handle user MFA options") logging.debug("User MFA options selected: [{}]".format( selected_mfa_option["factorType"])) if selected_mfa_option["factorType"] == "push": return push_approval(headers, mfa_challenge_url, payload) if settings.mfa_response is None: logging.debug("Getting verification code from user.") print("Type verification code and press Enter") settings.mfa_response = helpers.get_input() # time to verify the mfa method payload = helpers.prepare_payload(stateToken=primary_auth["stateToken"], passCode=settings.mfa_response) mfa_verify = okta_verify_api_method(mfa_challenge_url, payload, headers) logging.debug("mfa_verify [{}]".format(json.dumps(mfa_verify))) return mfa_verify
def get_duo_sid(duo_info): """Perform the initial Duo authentication request to obtain the SID. The SID is referenced throughout the authentication process for Duo. :param duo_info: dict response describing Duo factor in Okta. :return: duo_info with added SID. :return: duo_auth_response, contains html content listing available factors. """ params = helpers.prepare_payload( tx=duo_info["tx"], v=duo_info["version"], parent=duo_info["parent"] ) url = "https://{}/frame/web/v1/auth".format(duo_info["host"]) logging.info( "Calling Duo {} with params {}".format(urlparse(url).path, params.keys()) ) duo_auth_response = duo_api_post(url, params=params) try: duo_auth_redirect = urlparse("{}".format(unquote(duo_auth_response.url))).query duo_info["sid"] = duo_auth_redirect.strip("sid=") except Exception as sid_error: logging.error( "There was an error getting your SID." "Please try again. \n{}".format(sid_error) ) return duo_info, duo_auth_response
def duo_mfa_challenge(duo_info, mfa_option, passcode): """Poke Duo to challenge the selected factor. After the user has selected their device and factor of choice, tell Duo to send a challenge. This is where the end user will receive a phone call or push. :param duo_info: dict of parameters for Duo :param mfa_option: the user's selected second factor. :return txid: Duo transaction ID used to track this auth attempt. """ url = "https://{}/frame/prompt".format(duo_info["host"]) device = mfa_option["device"].split(" - ")[0] mfa_data = helpers.prepare_payload( factor=mfa_option["factor"], device=device, sid=duo_info["sid"], out_of_date=False, days_out_of_date=0, days_to_block=None, ) mfa_data["async"] = True # async is a reserved keyword if passcode: mfa_data["passcode"] = passcode mfa_challenge = duo_api_post(url, payload=mfa_data) txid = parse_duo_mfa_challenge(mfa_challenge) logging.debug("Sent MFA Challenge and obtained Duo transaction ID.") return txid
def user_mfa_challenge(headers, primary_auth): """Handle user mfa challenges. :param headers: headers what needs to be sent to api :param primary_auth: primary authentication :return: Okta MFA Session token after the successful entry of the code """ logging.debug("Handle user MFA challenges") mfa_options = primary_auth["_embedded"]["factors"] preset_mfa = settings.mfa_method available_mfas = [d["factorType"] for d in mfa_options] if available_mfas.count(preset_mfa) > 1: mfa_method = settings.mfa_method mfa_index = available_mfas.index(preset_mfa) provider = mfa_options[mfa_index]["provider"] mfa_id = mfa_options[mfa_index]["id"] logging.warning("\n\nMore than one method found with {}.\n" "Defaulting to {} - {} - Id: {}.\n" "This functionality will be deprecated in" "the next major release.\n".format( mfa_method, provider, mfa_method, mfa_id)) mfa_index = user_mfa_index(preset_mfa, available_mfas, mfa_options) # time to challenge the mfa option selected_mfa_option = mfa_options[mfa_index] logging.debug("Selected MFA is [{}]".format(selected_mfa_option)) mfa_challenge_url = selected_mfa_option["_links"]["verify"]["href"] payload = helpers.prepare_payload( stateToken=primary_auth["stateToken"], factorType=selected_mfa_option["factorType"], provider=selected_mfa_option["provider"], profile=selected_mfa_option["profile"], ) selected_factor = okta_verify_api_method(mfa_challenge_url, payload, headers) mfa_provider = selected_factor["_embedded"]["factor"]["provider"].lower() logging.debug("MFA Challenge URL: [{}] headers: {}".format( mfa_challenge_url, headers)) mfa_session_token = mfa_provider_type( mfa_provider, selected_factor, mfa_challenge_url, primary_auth, selected_mfa_option, headers, payload, ) return mfa_session_token
def authenticate_duo(selected_okta_factor): """Accomplish MFA via Duo. This is the main function that coordinates the Duo multifactor fetching, presentation, selection, challenge, and verification until making an Okta callback. :param selected_okta_factor: Duo factor information retrieved from Okta. :return payload: required payload for Okta callback :return headers: required headers for Okta callback """ try: duo_info = prepare_duo_info(selected_okta_factor) except KeyError as missing_key: logging.error( "There was an issue parsing the Okta factor." " Please try again. \n{}".format(missing_key) ) sys.exit(1) # Collect devices, factors, auth params for Duo duo_info, duo_auth_response = get_duo_sid(duo_info) factor_options = get_duo_devices(duo_auth_response) mfa_index = helpers.select_preferred_mfa_index( factor_options, factor_key="factor", subfactor_key="device" ) mfa_option = factor_options[mfa_index] logging.debug("Selected MFA is [{}]".format(mfa_option)) passcode = set_passcode(mfa_option) txid = duo_mfa_challenge(duo_info, mfa_option, passcode) verify_mfa = duo_mfa_verify(duo_info, txid) # Make factor callback to Duo sig_response = duo_factor_callback(duo_info, verify_mfa) # Prepare for Okta callback payload = helpers.prepare_payload( id=duo_info["factor_id"], sig_response=sig_response, stateToken=duo_info["state_token"], ) headers = {} headers["content-type"] = "application/json" headers["accept"] = "application/json" return payload, headers, duo_info["okta_callback_url"]
def duo_mfa_verify(duo_info, txid): """Verify MFA challenge completion. After the user has received the MFA challenge, query the Duo API until the challenge is completed. :param duo_info: dict of parameters for Duo. :param mfa_option: the user's selected second factor. :return txid: Duo transaction ID used to track this auth attempt. """ url = "https://{}/frame/status".format(duo_info["host"]) challenged_mfa = helpers.prepare_payload(txid=txid, sid=duo_info["sid"]) challenge_result = None while True: logging.debug("Waiting for MFA challenge response") mfa_result = duo_api_post(url, payload=challenged_mfa) verify_mfa = get_mfa_response(mfa_result) challenge_result, challenge_reason = parse_challenge( verify_mfa, challenge_result ) if challenge_result is None: continue elif challenge_result == "success": logging.debug("Successful MFA challenge received") break elif challenge_result == "failure": logging.critical( "MFA challenge has failed:" " {}. Please try again.".format(challenge_reason) ) sys.exit(2) else: logging.debug( "MFA challenge result: {}" "Reason: {}\n\n".format(challenge_result, challenge_reason) ) time.sleep(1) return verify_mfa
def user_mfa_challenge(headers, primary_auth): """Handle user mfa challenges. :param headers: headers what needs to be sent to api :param primary_auth: primary authentication :return: Okta MFA Session token after the successful entry of the code """ logging.debug("Handle user MFA challenges") try: mfa_options = primary_auth["_embedded"]["factors"] except KeyError: logging.error("Okta auth failed: " "Could not retrieve list of MFA methods") logging.debug("Error parsing response: {}".format( json.dumps(primary_auth))) sys.exit(1) mfa_setup_statuses = [ d["status"] for d in mfa_options if "status" in d and d["status"] != "ACTIVE" ] if len(mfa_setup_statuses) == len(mfa_options): logging.error("MFA not configured. " "Please enable MFA on your account and try again.") sys.exit(2) preset_mfa = settings.mfa_method available_mfas = [d["factorType"] for d in mfa_options] if preset_mfa is not None and preset_mfa in available_mfas: mfa_index = available_mfas.index(settings.mfa_method) else: logging.warning( "No MFA provided or provided MFA does not exist. [{}]".format( settings.mfa_method)) mfa_index = helpers.select_preferred_mfa_index(mfa_options) # time to challenge the mfa option selected_mfa_option = mfa_options[mfa_index] logging.debug("Selected MFA is [{}]".format(selected_mfa_option)) mfa_challenge_url = selected_mfa_option["_links"]["verify"]["href"] payload = helpers.prepare_payload( stateToken=primary_auth["stateToken"], factorType=selected_mfa_option["factorType"], provider=selected_mfa_option["provider"], profile=selected_mfa_option["profile"], ) selected_factor = okta_verify_api_method(mfa_challenge_url, payload, headers) mfa_provider = selected_factor["_embedded"]["factor"]["provider"].lower() logging.debug("MFA Challenge URL: [{}] headers: {}".format( mfa_challenge_url, headers)) if mfa_provider == "duo": payload, headers, callback_url = duo_helpers.authenticate_duo( selected_factor) okta_verify_api_method(callback_url, payload) payload.pop("id", "sig_response") mfa_verify = okta_verify_api_method(mfa_challenge_url, payload, headers) elif mfa_provider == "okta" or mfa_provider == "google": mfa_verify = user_mfa_options(selected_mfa_option, headers, mfa_challenge_url, payload, primary_auth) else: logging.error( "Sorry, the MFA provider '{}' is not yet supported." " Please retry with another option.".format(mfa_provider)) exit(1) return mfa_verify["sessionToken"]