diff --git a/README.md b/README.md index 75c8e56..aa6f081 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ aws-adfs integrates with: * SMS codes * Phone call * [Silverfort](https://www.silverfort.com/) MFA provider +* [Thales/SafeNet Trusted Access](https://cpl.thalesgroup.com/access-management/authentication) MFA provider + * OTP 6 digit codes generated by MobilePASS+ Authenticator app ## Setup Dependencies diff --git a/aws_adfs/_safenet_mfa.py b/aws_adfs/_safenet_mfa.py new file mode 100644 index 0000000..58135b5 --- /dev/null +++ b/aws_adfs/_safenet_mfa.py @@ -0,0 +1,84 @@ +import click +import lxml.etree as ET + +import logging +import re + +from . import run_command + +try: + # Python 3 + from urllib.parse import urlparse, parse_qs +except ImportError: + # Python 2 + from urlparse import urlparse, parse_qs + +from . import roles_assertion_extractor +from .helpers import trace_http_request + + +def extract(html_response, ssl_verification_enabled, mfa_token_command, mfa_token, session): + """ + :param response: raw http response + :param html_response: html result of parsing http response + :return: + """ + + roles_page_url = _action_url_on_validation_success(html_response) + + if mfa_token_command: + data = run_command.run_command(mfa_token_command) + safenet_mfa_code = data['mfa_token'] + logging.debug(f"using SafeNet MFA token from command: {safenet_mfa_code}") + elif mfa_token: + safenet_mfa_code = mfa_token + logging.debug(f"using SafeNet MFA token from env: {safenet_mfa_code}") + else: + safenet_mfa_code = click.prompt(text='Enter your SafeNet MFA token', type=str, hide_input=True) + + click.echo('Going for aws roles', err=True) + return _retrieve_roles_page( + roles_page_url, + _context(html_response), + session, + ssl_verification_enabled, + safenet_mfa_code, + ) + +def _context(html_response): + context_query = './/input[@name="Context"]' + element = html_response.find(context_query) + return element.get('value') + + +def _retrieve_roles_page(roles_page_url, context, session, ssl_verification_enabled, + safenet_mfa_code): + response = session.post( + roles_page_url, + verify=ssl_verification_enabled, + allow_redirects=True, + data={ + 'AuthMethod': 'SafeNet-MFA', + 'Context': context, + 'SAFENET_PASSWORD': safenet_mfa_code, + } + ) + trace_http_request(response) + + if response.status_code != 200: + raise click.ClickException( + u'Issues during redirection to aws roles page. The error response {}'.format( + response + ) + ) + + # Save session cookies to avoid having to repeat MFA on each login + session.cookies.save(ignore_discard=True) + + html_response = ET.fromstring(response.text, ET.HTMLParser()) + return roles_assertion_extractor.extract(html_response) + +def _action_url_on_validation_success(html_response): + safenet_mfa_auth_method = './/form[@id="options"]' + element = html_response.find(safenet_mfa_auth_method) + return element.get('action') diff --git a/aws_adfs/authenticator.py b/aws_adfs/authenticator.py index 71cbf39..2fba079 100644 --- a/aws_adfs/authenticator.py +++ b/aws_adfs/authenticator.py @@ -9,6 +9,7 @@ from . import _azure_mfa_authenticator as azure_mfa_auth from . import _azure_cloud_mfa_authenticator as azure_cloud_mfa_auth from . import _silverfort_authenticator as silverfort_mfa_auth +from . import _safenet_mfa as safenet_mfa from . import html_roles_fetcher from . import roles_assertion_extractor from .helpers import trace_http_request @@ -128,6 +129,11 @@ def _silverfort_extractor(): def extract(): return silverfort_mfa_auth.extract(html_response, config.ssl_verification, session) return extract + + def _safenet_extractor(): + def extract(): + return safenet_mfa.extract(html_response, config.ssl_verification, config.mfa_token_command, config.mfa_token, session) + return extract if assertfile is None: chosen_strategy = _plain_extractor @@ -148,6 +154,8 @@ def extract(): chosen_strategy = _azure_cloud_mfa_extractor elif _is_silverfort_mfa_authentication(html_response): chosen_strategy = _silverfort_extractor + elif _is_safenet_mfa_authentication(html_response): + chosen_strategy = _safenet_extractor return chosen_strategy() @@ -215,3 +223,11 @@ def _is_silverfort_mfa_authentication(html_response): element is not None and element.get('value') == 'SilverfortAdfs' ) + +def _is_safenet_mfa_authentication(html_response): + auth_method = './/input[@name="AuthMethod"]' + element = html_response.find(auth_method) + return ( + element is not None + and element.get('value') == 'SafeNet-MFA' + ) \ No newline at end of file