From 45577b9a376e9f03704f80dade5de4f4da8dc837 Mon Sep 17 00:00:00 2001 From: kudrinyaroslav <51086405+kudrinyaroslav@users.noreply.github.com> Date: Wed, 7 Jul 2021 23:34:34 +0300 Subject: [PATCH] add tests for authentication without mfa (#46) --- tests/okta_response_simulation.py | 39 +++++++++++ tests/samples.py | 109 ------------------------------ tests/unit_test.py | 81 ++++++++++++++++++---- tokendito/__version__.py | 2 +- tokendito/okta_helpers.py | 29 ++++---- 5 files changed, 124 insertions(+), 136 deletions(-) create mode 100644 tests/okta_response_simulation.py delete mode 100644 tests/samples.py diff --git a/tests/okta_response_simulation.py b/tests/okta_response_simulation.py new file mode 100644 index 00000000..6bbcf06c --- /dev/null +++ b/tests/okta_response_simulation.py @@ -0,0 +1,39 @@ +# vim: set filetype=python ts=4 sw=4 +# -*- coding: utf-8 -*- +"""simulation okta reponses.""" + +with_mfa = { + "status": "MFA_REQUIRED", + "_embedded": { + "user": { + "profile": { + "login": "Token.Dito@acme.org", + }, + }, + "factors": [ + { + "id": "opfrar9yi4bKJNH2WEWQ0x8", + "factorType": "push", + "provider": "OKTA", + "profile": {"name": "Redmi 6 Pro"}, + }, + { + "id": "FfdskljfdsS1ljUT0r8", + "factorType": "token:software:totp", + "provider": "GOOGLE", + "profile": {"credentialId": "Token.Dito@acme.org"}, + }, + { + "id": "fdsfsd6ewREr8", + "factorType": "token:software:totp", + "provider": "OKTA", + "profile": {"credentialId": "Token.Dito@acme.org"}, + }, + ], + }, +} +no_mfa_no_session_token = {"status": "SUCCESS", "sessionToken": None} +no_mfa = {"status": "SUCCESS", "sessionToken": 345} +error_dict = {"errorCode": "E0000004"} +empty_dict = {} +no_auth_methods = {"status": "MFA_REQUIRED"} diff --git a/tests/samples.py b/tests/samples.py deleted file mode 100644 index 21d33180..00000000 --- a/tests/samples.py +++ /dev/null @@ -1,109 +0,0 @@ -# vim: set filetype=python ts=4 sw=4 -# -*- coding: utf-8 -*- -"""This module handles samples.""" - - -def primary_auth( - last_name="Lastname", - first_name="Firstname", - email="Firstname.Lastname@acme.org", - timestamp=None, - time_zone=None, - locale=None, -): - """Generate template for simalation okta reply. - - :param first_name: User first name - :param last_name: User last name - :param email: User email - :param timestamp: localtime in user location - :param time_zone: user's time zone - :param locale: user's locale - :return: simulated okta reply - - """ - return { - "stateToken": "xfmktlTe4ksl593klssER", - "expiresAt": timestamp, - "status": "MFA_REQUIRED", - "factorResult": "SUCCESS", - "_embedded": { - "user": { - "id": "44urdfsafdse3Ib0x8", - "profile": { - "login": email, - "firstName": first_name, - "lastName": last_name, - "locale": locale, - "timeZone": time_zone, - }, - }, - "factors": [ - { - "id": "opfrar9yi4bKJNH2WEWQ0x8", - "factorType": "push", - "provider": "OKTA", - "vendorName": "OKTA", - "profile": { - "credentialId": email, - "deviceType": "SmartPhone_Android", - "keys": [ - { - "kty": "RSA", - "use": "sig", - "kid": "default", - "e": "AQAB", - "n": "FDSAKLJFDSALElkdfjsklj3424lkdsfjlkKLDJSF", - } - ], - "name": "Redmi 6 Pro", - "platform": "ANDROID", - "version": "28", - }, - "_links": { - "verify": { - "href": "https://www.acme.org", - "hints": {"allow": ["POST"]}, - } - }, - }, - { - "id": "FfdskljfdsS1ljUT0r8", - "factorType": "token:software:totp", - "provider": "GOOGLE", - "vendorName": "GOOGLE", - "profile": {"credentialId": email}, - "_links": { - "verify": { - "href": "https://www.acme.org", - "hints": {"allow": ["POST"]}, - } - }, - }, - { - "id": "fdsfsd6ewREr8", - "factorType": "token:software:totp", - "provider": "OKTA", - "vendorName": "OKTA", - "profile": {"credentialId": email}, - "_links": { - "verify": { - "href": "https://www.acme.org", - "hints": {"allow": ["POST"]}, - } - }, - }, - ], - "policy": { - "allowRememberDevice": True, - "rememberDeviceLifetimeInMinutes": 82200, - "rememberDeviceByDefault": False, - "factorsPolicyInfo": { - "opfrar9yi4bRM2NHV0x7": {"autoPushEnabled": False} - }, - }, - }, - "_links": { - "cancel": {"href": "https://www.acme.org", "hints": {"allow": ["POST"]}} - }, - } diff --git a/tests/unit_test.py b/tests/unit_test.py index 62d3932a..9e9acb02 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -31,7 +31,6 @@ from future import standard_library import pytest -from samples import primary_auth import semver from tokendito.settings import okta_status_dict @@ -106,7 +105,22 @@ def invalid_settings(): @pytest.fixture def sample_json_response(): """Return a response from okta server.""" - return primary_auth + from okta_response_simulation import no_mfa_no_session_token + from okta_response_simulation import no_mfa + from okta_response_simulation import error_dict + from okta_response_simulation import empty_dict + from okta_response_simulation import no_auth_methods + from okta_response_simulation import with_mfa + + okta_fixture_data = { + "okta_response_no_auth_methods": no_auth_methods, + "okta_response_empty": empty_dict, + "okta_response_error": error_dict, + "okta_response_no_mfa": no_mfa, + "okta_response_no_mfa_no_session_token": no_mfa_no_session_token, + "okta_response_mfa": with_mfa, + } + return okta_fixture_data @pytest.fixture @@ -379,20 +393,48 @@ def test_process_ini_file(tmpdir, valid_settings, invalid_settings, mocker): @pytest.mark.parametrize( - "status, session_token, expected", - [("SUCCESS", 123, 123), ("MFA_REQUIRED", 345, 345)], + "session_token, expected, mfa_availability", + [ + (345, 345, "okta_response_no_auth_methods"), + (345, 345, "okta_response_mfa"), + (345, 345, "okta_response_no_auth_methods"), + (None, None, "okta_response_no_mfa_no_session_token"), + ], ) -def test_user_session_token(status, session_token, expected, mocker, sample_headers): +def test_user_session_token( + sample_json_response, + session_token, + expected, + mocker, + sample_headers, + mfa_availability, +): """Test whether function return key on specific status.""" from tokendito.okta_helpers import user_session_token - primary_auth = {"status": status, "sessionToken": session_token} + primary_auth = sample_json_response[mfa_availability] + mocker.patch( "tokendito.okta_helpers.user_mfa_challenge", return_value=session_token ) assert user_session_token(primary_auth, sample_headers) == expected +def test_bad_user_session_token(sample_json_response, sample_headers, mocker): + """Test whether function behave accordingly.""" + from tokendito.okta_helpers import user_session_token + + mocker.patch("tokendito.okta_helpers.login_error_code_parser", return_value=None) + okta_response_statuses = ["okta_response_error", "okta_response_empty"] + + for response in okta_response_statuses: + + primary_auth = sample_json_response[response] + + with pytest.raises(SystemExit) as error: + assert user_session_token(primary_auth, sample_headers) == error + + @pytest.mark.parametrize( "mfa_provider, session_token, expected", [("duo", 123, 123), ("okta", 345, 345), ("google", 456, 456)], @@ -528,7 +570,8 @@ def test_user_mfa_index(preset_mfa, output, mocker, sample_json_response): """Test whether the function returns correct mfa method index.""" from tokendito.okta_helpers import user_mfa_index - primary_auth = sample_json_response() + primary_auth = sample_json_response["okta_response_mfa"] + mfa_options = primary_auth["_embedded"]["factors"] available_mfas = [d["factorType"] for d in mfa_options] mocker.patch("tokendito.helpers.select_preferred_mfa_index", return_value=1) @@ -540,8 +583,8 @@ def test_select_preferred_mfa_index(mocker, sample_json_response): """Test whether the function returns index entered by user.""" from tokendito.helpers import select_preferred_mfa_index - primary_auth = sample_json_response() - mfa_options = primary_auth.get("_embedded").get("factors") + primary_auth = sample_json_response + mfa_options = primary_auth["okta_response_mfa"]["_embedded"]["factors"] for output in mfa_options: mocker.patch("tokendito.helpers.collect_integer", return_value=output) assert select_preferred_mfa_index(mfa_options) == output @@ -550,19 +593,19 @@ def test_select_preferred_mfa_index(mocker, sample_json_response): @pytest.mark.parametrize( "email", [ - ("Firstname.Lastname@acme.org"), + ("Token.Dito@acme.org"), ], ) def test_select_preferred_mfa_index_output(email, capsys, mocker, sample_json_response): """Test whether the function gives correct output.""" from tokendito.helpers import select_preferred_mfa_index - primary_auth = sample_json_response(email=email) - mfa_options = primary_auth.get("_embedded").get("factors") + primary_auth = sample_json_response + mfa_options = primary_auth["okta_response_mfa"]["_embedded"]["factors"] correct_output = ( "\nSelect your preferred MFA method and press Enter:\n" - "[0] OKTA push Redmi 6 Pro Id: opfrar9yi4bKJNH2WEWQ0x8\n" + "[0] OKTA push Redmi 6 Pro Id: opfrar9yi4bKJNH2WEWQ0x8\n" "[1] GOOGLE token:software:totp {0} Id: FfdskljfdsS1ljUT0r8\n" "[2] OKTA token:software:totp {0} Id: fdsfsd6ewREr8\n".format(email) ) @@ -571,3 +614,15 @@ def test_select_preferred_mfa_index_output(email, capsys, mocker, sample_json_re select_preferred_mfa_index(mfa_options) captured = capsys.readouterr() assert captured.out == correct_output + + +def test_bad_with_no_mfa_methods_user_mfa_challenge( + sample_headers, sample_json_response +): + """Test whether okta response has mfa methods.""" + from tokendito.okta_helpers import user_mfa_challenge + + primary_auth = sample_json_response["okta_response_no_auth_methods"] + + with pytest.raises(SystemExit) as error: + assert user_mfa_challenge(sample_headers, primary_auth) == error diff --git a/tokendito/__version__.py b/tokendito/__version__.py index 10c44a56..bea8adb8 100644 --- a/tokendito/__version__.py +++ b/tokendito/__version__.py @@ -1,7 +1,7 @@ # vim: set filetype=python ts=4 sw=4 # -*- coding: utf-8 -*- """tokendito version.""" -__version__ = "1.2.0" +__version__ = "1.2.1" __title__ = "tokendito" __description__ = "Get AWS STS tokens from Okta SSO" __long_description_content_type__ = "text/x-rst" diff --git a/tokendito/okta_helpers.py b/tokendito/okta_helpers.py index d4e0af50..8b5d4714 100644 --- a/tokendito/okta_helpers.py +++ b/tokendito/okta_helpers.py @@ -54,10 +54,6 @@ def okta_verify_api_method(mfa_challenge_url, payload, headers=None): logging.debug("Received type of response: {}".format(type(response.text))) response = response.text - if "errorCode" in response: - login_error_code_parser(response["errorCode"], settings.okta_status_dict) - sys.exit(1) - return response @@ -87,16 +83,20 @@ def user_session_token(primary_auth, headers): param primary_auth: Primary authentication return session_token: Session Token from JSON response """ - try: - status = primary_auth.get("errorCode", primary_auth["status"]) - except KeyError: - logging.debug("Error parsing response: {}".format(json.dumps(primary_auth))) - logging.error("Okta auth failed: unknown status") - sys.exit(1) + status = None + if primary_auth.get("errorCode"): + status = primary_auth.get("errorCode") + else: + status = primary_auth.get("status") + if status == "SUCCESS": - session_token = primary_auth["sessionToken"] + session_token = primary_auth.get("sessionToken") elif status == "MFA_REQUIRED": session_token = user_mfa_challenge(headers, primary_auth) + elif status is None: + logging.debug("Error parsing response: {}".format(json.dumps(primary_auth))) + logging.error("Okta auth failed: unknown status") + sys.exit(1) else: login_error_code_parser(status, settings.okta_status_dict) sys.exit(1) @@ -126,7 +126,6 @@ def authenticate_user(okta_url, okta_username, okta_password): logging.debug("Authenticate Okta header [{}] ".format(headers)) session_token = user_session_token(primary_auth, headers) - logging.info("User has been succesfully authenticated.") return session_token @@ -192,8 +191,12 @@ def user_mfa_challenge(headers, primary_auth): :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 as error: + logging.error("There was a wrong response structure: \n{}".format(error)) + sys.exit(1) - mfa_options = primary_auth["_embedded"]["factors"] preset_mfa = settings.mfa_method available_mfas = [d["factorType"] for d in mfa_options]