From 521695542ae2efa8e895fd5ebe6dc050943bd437 Mon Sep 17 00:00:00 2001 From: pcmxgti <16561338+pcmxgti@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:11:39 -0500 Subject: [PATCH] Fix regression bug in MFA device recognition. (#145) * Fix regression bug that removed trusted device recongnition. * Fix User-Agent field to work with Okta. * Clean up cookie passing and handling, to simplify development. --- README.md | 39 +++---- tests/functional/test_auth.py | 13 +++ tests/unit/test_aws.py | 3 +- tests/unit/test_http_client.py | 37 ++++++- tests/unit/test_okta.py | 25 +++-- tokendito/__init__.py | 2 +- tokendito/aws.py | 11 +- tokendito/http_client.py | 28 ++++- tokendito/okta.py | 192 ++++++++------------------------- tokendito/user.py | 23 +++- 10 files changed, 185 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index 99d2b23b..0b24bd94 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,18 @@ your AWS accounts, returning tokens into your local `~/.aws/credentials` file. ## What's new + See [Releases](https://github.com/dowjones/tokendito/releases) for a detailed Changelog. + ### Tokendito 2.3.0 + Version 2.3.0 of Tokendito introduces the following new features: -- Basic OIE support while forcing Classic mode. + +- Basic OIE support while forcing Classic mode. - Misc bug fixes Note: This feature currently works with locally enabled OIE organizations, but it does not for Organizations with chained Authentication in mixed OIE/Classic environments. - ### Tokendito 2.2.0 Version 2.2.0 of Tokendito introduces the following new features: @@ -40,7 +43,6 @@ Version 2.2.0 of Tokendito introduces the following new features: - Support for Step-Up Authorization (by @ruhulio) - Misc bug fixes - ### Tokendito 2.1.0 Version 2.1.0 of Tokendito introduces the following new features: @@ -51,9 +53,9 @@ Version 2.1.0 of Tokendito introduces the following new features: - Docker container signing to ensure you are on a 'certified' Tokendito container - Misc bug fixes - ### Tokendito 2.0.0 -With the release of tokendito 2.0, many changes and fixes were introduced. **It is a breaking release**: your configuration needs to be updated, the command line arguments have changed, and support for Python < 3.7 has been removed. + +With the release of tokendito 2.0, many changes and fixes were introduced. **It is a breaking release**: your configuration needs to be updated, the command line arguments have changed, and support for Python \< 3.7 has been removed. The following changes are part of this release: - Set the config file to be platform dependent, and follow the XDG standard. @@ -71,17 +73,17 @@ Consult [additional notes](https://github.com/dowjones/tokendito/blob/main/docs/ ## Requirements -- Python 3.7+, or a working Docker environment -- AWS account(s) federated with Okta +- Python 3.7+, or a working Docker environment +- AWS account(s) federated with Okta Tokendito is compatible with Python 3 and can be installed with either pip or pip3. ## Getting started -1. Install (via PyPi): `pip install tokendito` -2. Run `tokendito --configure`. -3. Run `tokendito`. +1. Install (via PyPi): `pip install tokendito` +1. Run `tokendito --configure`. +1. Run `tokendito`. **NOTE**: Advanced users may shorten the `tokendito` interaction to a [single command](https://github.com/dowjones/tokendito/blob/main/docs/README.md#single-command-usage). @@ -89,7 +91,6 @@ command](https://github.com/dowjones/tokendito/blob/main/docs/README.md#single-c Have multiple Okta tiles to switch between? View our [multi-tile guide](https://github.com/dowjones/tokendito/blob/main/docs/README.md#multi-tile-guide). - ## Docker Using Docker eliminates the need to install tokendito and its requirements. We are providing experimental Docker image support in [Dockerhub](https://hub.docker.com/r/tokendito/tokendito) @@ -98,13 +99,13 @@ Using Docker eliminates the need to install tokendito and its requirements. We a Run tokendito with the `docker run` command. Tokendito supports [DCT](https://docs.docker.com/engine/security/trust/), and we encourage you to enforce image signature validation before running any containers. -``` shell +```shell export DOCKER_CONTENT_TRUST=1 ``` then -``` shell +```shell docker run --rm -it tokendito/tokendito --version ``` @@ -118,19 +119,21 @@ These can be covered by mapping a single volume to both the host and container u Be sure to set the `-it` flags to enable an interactive terminal session. On Windows, you can do the following: -``` powershell + +```powershell docker run --rm -it -v "%USERPROFILE%\.aws":/app/.aws -v "%USERPROFILE%\.config":/app/.config tokendito/tokendito ``` In a Mac OS system, you can run: -``` shell + +```shell docker run --rm -it -v "$HOME/.aws":/app/.aws -v "$HOME/.config":/app/.config tokendito/tokendito ``` On a Linux system, however, you must specify the user and group IDs for the mount mappings to work as expected. Additionally the mount points within the container move to a different location: -``` shell +```shell docker run --user $(id -u):$(id -g) --rm -it -v "$HOME/.aws":/.aws -v "$HOME/.config":/.config tokendito/tokendito ``` @@ -138,7 +141,7 @@ Tokendito command line arguments are supported as well. **NOTE**: In the following examples the entire home directory is exported for simplicity. This is not recommended as it exposes too much data to the running container: -``` shell +```shell docker run --rm -it -v "$HOME":/ tokendito/tokendito \ --okta-tile https://acme.okta.com/home/amazon_aws/000000000000000000x0/123 \ --username username@example.com \ @@ -151,7 +154,7 @@ docker run --rm -it -v "$HOME":/ tokendito/tokendito \ Tokendito profiles are supported while using containers provided the proper volume mapping exists. -``` shell +```shell docker run --rm -ti -v "$HOME":/app tokendito/tokendito \ --profile my-profile-name ``` diff --git a/tests/functional/test_auth.py b/tests/functional/test_auth.py index 65295b68..409579f8 100644 --- a/tests/functional/test_auth.py +++ b/tests/functional/test_auth.py @@ -73,6 +73,9 @@ def test_generate_credentials(custom_args, config_file): f"{config.okta['username']}", "--password", f"{config.okta['password']}", + "--config-file", + f"{config.user['config_file']}", + "--use-device-token", "--loglevel", "DEBUG", ] @@ -87,6 +90,16 @@ def test_generate_credentials(custom_args, config_file): assert '"sessionToken": "*****"' in proc["stderr"] assert proc["exit_status"] == 0 + # Ensure the device token is written to the config file, and is correct. + device_token = None + match = re.search(r"(?<=okta_device_token': ')[^']+", proc["stderr"]) + if match: + device_token = match.group(0) + with open(config.user["config_file"]) as cfg: + assert f"okta_device_token = {device_token}" in cfg.read() + + # print(f"stderr: {proc['stderr']}") + @pytest.mark.run("second") def test_aws_credentials(custom_args): diff --git a/tests/unit/test_aws.py b/tests/unit/test_aws.py index 25c6a426..e4743f99 100644 --- a/tests/unit/test_aws.py +++ b/tests/unit/test_aws.py @@ -110,7 +110,6 @@ def test_authenticate_to_roles(status_code, monkeypatch): "org": "https://acme.okta.org/", } ) - cookies = {"some_cookie": "some_value"} with pytest.raises(SystemExit): - authenticate_to_roles(pytest_config, [("http://test.url.com", "")], cookies) + authenticate_to_roles(pytest_config, [("http://test.url.com", "")]) diff --git a/tests/unit/test_http_client.py b/tests/unit/test_http_client.py index 46058332..d4d8f5e0 100644 --- a/tests/unit/test_http_client.py +++ b/tests/unit/test_http_client.py @@ -17,6 +17,33 @@ def client(): return HTTPClient() +@pytest.mark.parametrize( + "base_os, expected", + [ + ("Darwin", "Macintosh"), + ("Linux", "X11"), + ("Windows", "Windows"), + ("Unknown", "compatible"), + ], +) +def test_generate_user_agent(mocker, base_os, expected): + """Test the generate_user_agent function.""" + import platform + + from tokendito.http_client import generate_user_agent + + mocker.patch("platform.uname", return_value=(base_os, "", "pytest", "", "", "")) + python_version = platform.python_version() + + user_agent = generate_user_agent() + assert user_agent == ( + f"{__title__}/{__version__} " + f"({expected}; {base_os}/pytest) " + f"Python/{python_version}; " + f"requests/{requests.__version__})" + ) + + def test_init(client): """Test initialization of HTTPClient instance.""" # Check if the session property of the client is an instance of requests.Session @@ -24,7 +51,7 @@ def test_init(client): # Check if the User-Agent header was set correctly during initialization expected_user_agent = f"{__title__}/{__version__}" - assert client.session.headers["User-Agent"] == expected_user_agent + assert str(expected_user_agent) in str(client.session.headers["User-Agent"]) def test_set_cookies(client): @@ -166,6 +193,10 @@ def test_get_device_token(client): # Check if the device token is set correctly in the session assert client.get_device_token() == device_token + # Check no device token when the cookie is not set + client.session.cookies.clear() + assert client.get_device_token() is None + def test_set_device_token(client): """Test setting device token in the session.""" @@ -174,3 +205,7 @@ def test_set_device_token(client): # Check if the device token is set correctly in the session assert client.session.cookies.get("DT") == device_token + + # Check no device token set when the cookie is not set + client.session.cookies.clear() + assert client.set_device_token("http://test.com", None) is None diff --git a/tests/unit/test_okta.py b/tests/unit/test_okta.py index 1b241a79..32281e62 100644 --- a/tests/unit/test_okta.py +++ b/tests/unit/test_okta.py @@ -4,6 +4,7 @@ from unittest.mock import Mock import pytest +import requests.cookies from tokendito.config import Config from tokendito.http_client import HTTP_client @@ -304,11 +305,11 @@ def test_push_approval(mocker, return_value, side_effect, expected): ({"type": "SAML2"}, False), ], ) -def test_is_local_auth(auth_properties, expected): +def test_local_authentication_enabled(auth_properties, expected): """Test local auth method.""" from tokendito import okta - assert okta.is_local_auth(auth_properties) == expected + assert okta.local_authentication_enabled(auth_properties) == expected @pytest.mark.parametrize( @@ -486,8 +487,12 @@ def test_send_saml_response(mocker): from tokendito.config import Config from tokendito.http_client import HTTP_client + cookies = requests.cookies.RequestsCookieJar() + cookies.set("sid", "pytestcookie") mock_response = Mock() - mock_response.cookies = {"sid": "pytestcookie"} + mock_response.status_code = 201 + mock_response.session = Mock() + mock_response.session.cookies = cookies saml_response = { "response": "pytestresponse", @@ -501,7 +506,7 @@ def test_send_saml_response(mocker): pytest_config = Config() - assert okta.send_saml_response(pytest_config, saml_response) == mock_response.cookies + assert okta.send_saml_response(pytest_config, saml_response) is None def test_idp_auth(mocker): @@ -522,10 +527,10 @@ def test_idp_auth(mocker): mocker.patch("tokendito.okta.saml2_authenticate", return_value=sid) mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "OKTA"}) - assert okta.idp_auth(pytest_config) == sid + assert okta.idp_auth(pytest_config) is None mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "SAML2"}) - assert okta.idp_auth(pytest_config) == sid + assert okta.idp_auth(pytest_config) is None mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "UNKNOWN"}) with pytest.raises(SystemExit) as error: @@ -585,7 +590,7 @@ def test_step_up_authenticate(mocker): assert okta.step_up_authenticate(pytest_config, state_token) is False -def test_local_auth(mocker): +def test_local_authenticate(mocker): """Test local auth method.""" from tokendito import okta from tokendito.config import Config @@ -606,7 +611,7 @@ def test_local_auth(mocker): } ) - assert okta.local_auth(pytest_config) == "pytesttoken" + assert okta.local_authenticate(pytest_config) == "pytesttoken" def test_saml2_authenticate(mocker): @@ -634,5 +639,5 @@ def test_saml2_authenticate(mocker): } mocker.patch("tokendito.okta.send_saml_request", return_value=saml_response) - mocker.patch("tokendito.okta.send_saml_response", return_value="pytestsessionid") - assert okta.saml2_authenticate(pytest_config, auth_properties) == "pytestsessionid" + mocker.patch("tokendito.okta.send_saml_response", return_value=None) + assert okta.saml2_authenticate(pytest_config, auth_properties) is None diff --git a/tokendito/__init__.py b/tokendito/__init__.py index 984da438..450fbdf4 100644 --- a/tokendito/__init__.py +++ b/tokendito/__init__.py @@ -1,7 +1,7 @@ # vim: set filetype=python ts=4 sw=4 # -*- coding: utf-8 -*- """Tokendito module initialization.""" -__version__ = "2.3.0" +__version__ = "2.3.1" __title__ = "tokendito" __description__ = "Get AWS STS tokens from Okta SSO" __long_description_content_type__ = "text/markdown" diff --git a/tokendito/aws.py b/tokendito/aws.py index 6e14fd7a..d7ada323 100644 --- a/tokendito/aws.py +++ b/tokendito/aws.py @@ -47,11 +47,11 @@ def get_output_types(): return ["json", "text", "csv", "yaml", "yaml-stream"] -def authenticate_to_roles(config, urls, cookies): +def authenticate_to_roles(config, urls): """Authenticate AWS user with saml. - :param urls: list of tuples or tuple, with tiles info - :param cookies: html cookies + :param config: configuration object + :param urls: list of tuples or tuple, with tiles information :return: response text """ @@ -63,7 +63,8 @@ def authenticate_to_roles(config, urls, cookies): logger.info(f"Discovering roles in {tile_count} tile{plural}.") for url, label in url_list: session_url = config.okta["org"] + "/login/sessionCookieRedirect" - params = {"token": cookies.get("sessionToken"), "redirectUrl": url} + token = HTTP_client.session.cookies.get("sessionToken", None) + params = {"token": token, "redirectUrl": url} response = HTTP_client.get(session_url, params=params) saml_response_string = response.text @@ -74,7 +75,7 @@ def authenticate_to_roles(config, urls, cookies): if "Extra Verification" in saml_response_string and state_token: logger.info(f"Step-Up authentication required for {url}.") if okta.step_up_authenticate(config, state_token): - return authenticate_to_roles(config, urls, cookies) + return authenticate_to_roles(config, urls) logger.error("Step-Up Authentication required, but not supported.") elif "App Access Locked" in saml_response_string: diff --git a/tokendito/http_client.py b/tokendito/http_client.py index dd7cb7dc..6c5e9629 100644 --- a/tokendito/http_client.py +++ b/tokendito/http_client.py @@ -3,6 +3,7 @@ """This module handles HTTP client operations.""" import logging +import platform import sys from urllib.parse import urlparse @@ -13,12 +14,37 @@ logger = logging.getLogger(__name__) +def generate_user_agent(): + """Generate a user agent string.""" + python_version = platform.python_version() + (system, _, release, _, _, _) = platform.uname() + + base_os = "compatible" + if system == "Darwin": + base_os = "Macintosh" + elif system == "Linux": + base_os = "X11" + elif system == "Windows": + base_os = "Windows" + else: + logger.warning(f"Unknown platform: {system}") + + user_agent = ( + f"{__title__}/{__version__} " + f"({base_os}; {system}/{release}) " + f"Python/{python_version}; " + f"requests/{requests.__version__})" + ) + logger.debug(f"User agent: {user_agent}") + return user_agent + + class HTTPClient: """Handles HTTP client operations.""" def __init__(self): """Initialize the HTTPClient with a session object.""" - user_agent = f"{__title__}/{__version__}" + user_agent = generate_user_agent() self.session = requests.Session() self.session.headers.update({"User-Agent": user_agent}) diff --git a/tokendito/okta.py b/tokendito/okta.py index 8432b85c..acbd3cb6 100644 --- a/tokendito/okta.py +++ b/tokendito/okta.py @@ -17,13 +17,13 @@ import re import sys import time -import urllib +from urllib.parse import urlencode from urllib.parse import urlparse import uuid import bs4 from bs4 import BeautifulSoup -import requests +import requests.cookies from tokendito import duo from tokendito import user from tokendito.http_client import HTTP_client @@ -156,13 +156,6 @@ def send_saml_request(saml_request): :param cookies: session cookies with `sid` :returns: dict with with SP post_url, relay_state, and saml_response """ - logger.debug( - f""" - - HTTP_client cookies is {HTTP_client.session.cookies}") - - """ - ) # Define the payload and headers for the request payload = { "relayState": saml_request["relay_state"], @@ -193,12 +186,6 @@ def send_saml_request(saml_request): # Mask sensitive values for logging purposes user.add_sensitive_value_to_be_masked(saml_response["response"]) - logger.debug( - f""" - we have HTTP_client.session cookies: {HTTP_client.session.cookies} - """ - ) - # Return the formed SAML response return saml_response @@ -222,8 +209,16 @@ def set_oauth2_redirect_params_cookies(config, url): "scopes": get_authorize_scope(), "okta-oauth-state": get_oauth2_state(), } - cookies = {"okta-oauth-redirect-params": urllib.parse.urlencode(oauth2_config_reformatted)} - HTTP_client.set_cookies(cookies) + + cookiejar = requests.cookies.RequestsCookieJar() + domain = urlparse(url).netloc + cookiejar.set( + "okta-oauth-redirect-params", + urlencode(oauth2_config_reformatted), + domain=domain, + path="/", + ) + HTTP_client.set_cookies(cookiejar) def send_saml_response(config, saml_response): @@ -245,57 +240,38 @@ def send_saml_response(config, saml_response): url = saml_response["post_url"] # Log the SAML response details. - logger.debug( - f""" - Sending SAML response back to {url} - - HTTP_client session cookies is {HTTP_client.session.cookies} - """ - ) - + logger.debug(f"Sending SAML response to {url}") # Use the HTTP client to make a POST request. response = HTTP_client.post(url, data=payload, headers=headers) - # Extract cookies from the response. - session_cookies = response.cookies - # Get the 'sid' value from the cookies. - sid = session_cookies.get("sid") - logger.debug(f" new sid is {sid}") + sid = HTTP_client.session.cookies.get("sid", None) + logger.debug(f"New sid is {sid}") # If 'sid' is present, mask its value for logging purposes. - if sid is not None: + if sid: user.add_sensitive_value_to_be_masked(sid) else: logger.debug("We did not find a 'sid' entry in the cookies.") - # Log the session cookies. - logger.debug( - f""" - saml call to {url} - response cookies: {session_cookies} - """ - ) # Extract the state token from the response. state_token = extract_state_token(response.text) - if state_token: # TODO: this is not working yet. + # TODO: this is not working yet. + if state_token: if config.okta["client_id"] is not None: set_oauth2_redirect_params_cookies(config, config.okta["org"]) - myresponse = HTTP_client.get( - # myurl, allow_redirects=False, params={"stateToken": state_token} + response = HTTP_client.get( f"{config.okta['org']}/login/token/redirect", params={"stateToken": state_token}, ) logger.debug( f"State token from {url}: {state_token} - FIXME bring this back the calling stack" ) - session_cookies = myresponse.cookies - logger.debug(f"We return session_cookies: {session_cookies}") - - # Return the session cookies. - return session_cookies + # Return the response. + # TODO: this is not working yet. + return def get_session_token(config, primary_auth, headers): @@ -327,7 +303,7 @@ def get_session_token(config, primary_auth, headers): return session_token -def get_oauth2_token(config, authz_code_flow_data, authorize_code): +def get_oauth2_token(authz_code_flow_data, authorize_code): """Get OAuth token from Okta by calling /token endpoint. :param url: URL of the Okta OAuth token endpoint @@ -343,15 +319,10 @@ def get_oauth2_token(config, authz_code_flow_data, authorize_code): } headers = {"accept": "application/json"} # Using the http_client to make the POST request - response_json = HTTP_client.post( + response = HTTP_client.post( authz_code_flow_data["token_endpoint_url"], data=payload, headers=headers, return_json=True ) - return response_json - if "access_token" not in response_json: - logger.error(f"error getting token from {authz_code_flow_data['token_endpoint_url']}") - sys.exit(1) - - return response_json["access_token"] + return response def get_client_id(config): @@ -463,12 +434,8 @@ def get_authorize_code(response, payload): error_code = re.search(r"(?<=error=)[^&]+", callback_url) error_desc = re.search(r"(?<=error_description=)[^&]+", callback_url) if error_code: - logger.error( - f""" - oath2 callback error:{error_code.group()} - description:{error_desc.group()} - payload sent: {payload} - """ - ) + logger.error(f"Oauth2 callback error:{error_code.group()}:{error_desc.group()}") + logger.debug(f"Response: {response.text}") sys.exit(1) authorize_code = re.search(r"(?<=code=)[^&]+", callback_url) if authorize_code: @@ -506,9 +473,6 @@ def authorization_code_request(config, authz_code_flow_data): ) authorize_code = get_authorize_code(response, payload) - - logger.debug(f"Cookies in session: {HTTP_client.session.cookies}") - return authorize_code @@ -540,9 +504,9 @@ def authorization_code_flow(config, oauth2_config): authz_code_flow_data["code_challenge_method"] = get_pkce_code_challenge_method() authorize_code = authorization_code_request(config, authz_code_flow_data) - get_oauth2_token(config, authz_code_flow_data, authorize_code) + get_oauth2_token(authz_code_flow_data, authorize_code) - return HTTP_client.session.cookies + return def authorization_code_enabled(org_url, oauth2_config): @@ -610,16 +574,14 @@ def oauth2_authorize(config): Returns authz token """ - logger.debug(f"oie_authorize({config}") - oauth2_config = get_oauth2_configuration(config.okta["org"]) if authorization_code_enabled(config.okta["org"], oauth2_config): - cookies = authorization_code_flow(config, oauth2_config) + authorization_code_flow(config, oauth2_config) else: logger.warning( f"Authorization Code is not enabled on {config.okta['org']}, skipping oauth2" ) - return cookies + return def create_authn_cookies(authn_org_url, session_token): @@ -642,16 +604,19 @@ def create_authn_cookies(authn_org_url, session_token): # Use the HTTP client to make a POST request. response_json = HTTP_client.post(url, json=data, headers=headers, return_json=True) + if "id" not in response_json: logger.error(f"'id' not found in response. Full response: {response_json}") sys.exit(1) session_id = response_json["id"] user.add_sensitive_value_to_be_masked(session_id) - cookies = requests.cookies.RequestsCookieJar() + + cookiejar = requests.cookies.RequestsCookieJar() domain = urlparse(url).netloc - cookies.set("sid", session_id, domain=urlparse(url).netloc, path="/") - cookies.set("sessionToken", session_token, domain=domain, path="/") - return cookies + cookiejar.set("sid", session_id, domain=urlparse(url).netloc, path="/") + cookiejar.set("sessionToken", session_token, domain=domain, path="/") + HTTP_client.set_cookies(cookiejar) + return cookiejar def idp_auth(config): @@ -672,52 +637,28 @@ def idp_auth(config): logger.error("Okta auth failed: unknown type.") sys.exit(1) - logger.debug(f"GOING TO {config.okta['org']}") - if is_saml2_authentication(auth_properties): # We may loop thru the saml2 servers until # we find the authentication server. - session_cookies = saml2_authenticate(config, auth_properties) - HTTP_client.set_cookies(session_cookies) - logger.debug( - f""" - We just went thru saml2_authenticate - cookies are {HTTP_client.session.cookies} - """ - ) + saml2_authenticate(config, auth_properties) elif local_authentication_enabled(auth_properties): session_token = local_authenticate(config) # authentication sends us a token # which we then put in our session cookies - - HTTP_client.session.cookies = create_authn_cookies(config.okta["org"], session_token) - logger.debug( - f""" - authenticated via local_authenticate - - http session cookies are {HTTP_client.session.cookies} - """ - ) + create_authn_cookies(config.okta["org"], session_token) else: logger.error(f"{auth_properties['type']} login via IdP Discovery is not curretly supported") sys.exit(1) # Once we get there, the user is authenticated. - if "client_id" in config.okta and config.okta["client_id"] is not None: # If the user passed a client-id value, # we will run the oauth2 authorize flow on OIE enabled okta # and we will then get an idx cookies if oie_enabled(config.okta["org"]): - logger.debug( - f""" - session_cookies: {HTTP_client.session.cookies} - """ - ) - HTTP_client.session.cookies = oauth2_authorize(config) + oauth2_authorize(config) - logger.debug(f"Returning session cookies: {HTTP_client.session.cookies}") - return HTTP_client.session.cookies + return def step_up_authenticate(config, state_token): @@ -728,7 +669,7 @@ def step_up_authenticate(config, state_token): :return: True if step up authentication was successful; False otherwise """ auth_properties = get_auth_properties(userid=config.okta["username"], url=config.okta["org"]) - if "type" not in auth_properties or not is_local_auth(auth_properties): + if "type" not in auth_properties or not local_authentication_enabled(auth_properties): return False headers = {"content-type": "application/json", "accept": "application/json"} @@ -749,20 +690,6 @@ def step_up_authenticate(config, state_token): return False -def is_local_auth(auth_properties): - """Check whether authentication happens locally. - - :param auth_properties: auth_properties dict - :return: True for local auth, False otherwise. - """ - try: - if auth_properties["type"] == "OKTA": - return True - except (TypeError, KeyError): - pass - return False - - def is_saml2_auth(auth_properties): """Check whether authentication happens via SAML2 on a different IdP. @@ -777,33 +704,6 @@ def is_saml2_auth(auth_properties): return False -def local_auth(config): - """Authenticate local user with okta credential. - - :param config: Config object - :return: MFA session with options - """ - session_token = None - headers = {"content-type": "application/json", "accept": "application/json"} - payload = {"username": config.okta["username"], "password": config.okta["password"]} - - logger.debug(f"Authenticate user to {config.okta['org']}") - logger.debug(f"Sending {headers}, {payload} to {config.okta['org']}") - - primary_auth = HTTP_client.post( - f"{config.okta['org']}/api/v1/authn", json=payload, headers=headers, return_json=True - ) - - if "errorCode" in primary_auth: - api_error_code_parser(primary_auth["errorCode"]) - sys.exit(1) - - while session_token is None: - session_token = get_session_token(config, primary_auth, headers) - logger.info(f"User has been successfully authenticated to {config.okta['org']}.") - return session_token - - def saml2_authenticate(config, auth_properties): """SAML2 authentication flow. @@ -822,7 +722,7 @@ def saml2_authenticate(config, auth_properties): # Try to authenticate using the new configuration. This could cause # recursive calls, which allows for IdP chaining. - session_cookies = idp_auth(saml2_config) + idp_auth(saml2_config) # Once we are authenticated, send the SAML request to the IdP. # This call requires session cookies. @@ -830,8 +730,8 @@ def saml2_authenticate(config, auth_properties): # Send SAML response from the IdP back to the SP, which will generate new # session cookies. - session_cookies = send_saml_response(config, saml_response) - return session_cookies + send_saml_response(config, saml_response) + return def oie_enabled(url): diff --git a/tokendito/user.py b/tokendito/user.py index 96444421..f5cc1a47 100644 --- a/tokendito/user.py +++ b/tokendito/user.py @@ -64,8 +64,18 @@ def cmd_interface(args): ) sys.exit(1) + if config.user["use_device_token"]: + device_token = config.okta["device_token"] + if device_token: + HTTP_client.set_device_token(config.okta["org"], device_token) + else: + logger.warning( + f"Device token unavailable for config profile {args.user_config_profile}. " + "May see multiple MFA requests this time." + ) + # get authentication and authorization cookies from okta - session_cookies = okta.idp_auth(config) + okta.idp_auth(config) logger.debug( f""" about to call discover_tile @@ -79,7 +89,7 @@ def cmd_interface(args): config.okta["tile"] = discover_tiles(config.okta["org"]) # Authenticate to AWS roles - auth_tiles = aws.authenticate_to_roles(config, config.okta["tile"], session_cookies) + auth_tiles = aws.authenticate_to_roles(config, config.okta["tile"]) (role_response, role_name) = aws.select_assumeable_role(auth_tiles) @@ -99,6 +109,12 @@ def cmd_interface(args): output=config.aws["output"], ) + device_token = HTTP_client.get_device_token() + if config.user["use_device_token"] and device_token: + logger.info(f"Saving device token to config profile {args.user_config_profile}") + config.okta["device_token"] = device_token + update_device_token(config) + display_selected_role(profile_name=config.aws["profile"], role_response=role_response) @@ -353,7 +369,7 @@ def select_role_arn(authenticated_tiles): if roles.count(config.aws["profile"]) > 1: logger.error( "There are multiple matches for the profile selected, " - "please use the --role-arn option to select one" + "please use the --aws-role-arn option to select one" ) sys.exit(2) @@ -702,7 +718,6 @@ def process_arguments(args): pattern = re.compile(r"^(.*?)_(.*)") for key, val in vars(args).items(): - logger.debug(f"key is {key} and val is {val}") match = re.search(pattern, key.lower()) if match: if match.group(1) not in get_submodule_names():