Skip to content

Commit df9d5d7

Browse files
author
Mark Heiges
committed
add support for mfa_token env and mfa-token-command
1 parent f0f985b commit df9d5d7

8 files changed

+86
-47
lines changed

README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,12 @@ aws-adfs integrates with:
252252
of a shell command (expected JSON format:
253253
`{"username": "myusername", "password":
254254
"mypassword"}`)
255-
--env Read username, password from environment
256-
variables (username and password).
255+
--mfa-token-command TEXT Read MFA token from the output of a shell
256+
command (expected JSON format:
257+
`{"mfa_token": "123654"}`)
258+
--env Read username, password and optionally an
259+
MFA token from environment variables
260+
(username, password and mfa_token).
257261
--stdin Read username, password from standard input
258262
separated by a newline.
259263
--authfile TEXT Read username, password from a local file

aws_adfs/_rsa_authenticator.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import logging
55
import re
66

7+
from . import run_command
8+
79
try:
810
# Python 3
911
from urllib.parse import urlparse, parse_qs
@@ -15,7 +17,7 @@
1517
from .helpers import trace_http_request
1618

1719

18-
def extract(html_response, ssl_verification_enabled, session):
20+
def extract(html_response, ssl_verification_enabled, mfa_token_command, mfa_token, session):
1921
"""
2022
:param response: raw http response
2123
:param html_response: html result of parsing http response
@@ -24,7 +26,15 @@ def extract(html_response, ssl_verification_enabled, session):
2426

2527
roles_page_url = _action_url_on_validation_success(html_response)
2628

27-
rsa_securid_code = click.prompt(text='Enter your RSA SecurID token', type=str, hide_input=True)
29+
if mfa_token_command:
30+
data = run_command.run_command(mfa_token_command)
31+
rsa_securid_code = data['mfa_token']
32+
logging.debug(f"using RSA SecurID token from command: {rsa_securid_code}")
33+
elif mfa_token:
34+
rsa_securid_code = mfa_token
35+
logging.debug(f"using RSA SecurID token from env: {rsa_securid_code}")
36+
else:
37+
rsa_securid_code = click.prompt(text='Enter your RSA SecurID token', type=str, hide_input=True)
2838

2939
click.echo('Going for aws roles', err=True)
3040
return _retrieve_roles_page(

aws_adfs/_symantec_vip_access.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import logging
55
import re
66

7+
from . import run_command
8+
79
try:
810
# Python 3
911
from urllib.parse import urlparse, parse_qs
@@ -15,7 +17,7 @@
1517
from .helpers import trace_http_request
1618

1719

18-
def extract(html_response, ssl_verification_enabled, session):
20+
def extract(html_response, ssl_verification_enabled, mfa_token_command, mfa_token, session):
1921
"""
2022
:param response: raw http response
2123
:param html_response: html result of parsing http response
@@ -24,7 +26,15 @@ def extract(html_response, ssl_verification_enabled, session):
2426

2527
roles_page_url = _action_url_on_validation_success(html_response)
2628

27-
vip_security_code = click.prompt(text='Enter your Symantec VIP Access code', type=str)
29+
if mfa_token_command:
30+
data = run_command.run_command(mfa_token_command)
31+
vip_security_code = data['mfa_token']
32+
logging.debug(f"using VIP Access Code from command: {vip_security_code}")
33+
elif mfa_token:
34+
vip_security_code = mfa_token
35+
logging.debug(f"using VIP Access Code from env: {vip_security_code}")
36+
else:
37+
vip_security_code = click.prompt(text='Enter your Symantec VIP Access code', type=str)
2838

2939
click.echo('Going for aws roles', err=True)
3040
return _retrieve_roles_page(

aws_adfs/authenticator.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def extract():
101101

102102
def _symantec_vip_extractor():
103103
def extract():
104-
return symantec_vip_access.extract(html_response, config.ssl_verification, session)
104+
return symantec_vip_access.extract(html_response, config.ssl_verification, config.mfa_token_command, config.mfa_token, session)
105105
return extract
106106

107107
def _file_extractor():
@@ -111,7 +111,7 @@ def extract():
111111

112112
def _rsa_auth_extractor():
113113
def extract():
114-
return rsa_auth.extract(html_response, config.ssl_verification, session)
114+
return rsa_auth.extract(html_response, config.ssl_verification, config.mfa_token_command, config.mfa_token, session)
115115
return extract
116116

117117
def _azure_mfa_extractor():

aws_adfs/login.py

+10-39
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from botocore import client
2020

2121
from . import authenticator, helpers, prepare, role_chooser
22+
from . import run_command
2223
from .consts import (
2324
DUO_UNIVERSAL_PROMPT_FACTOR_DUO_PUSH,
2425
DUO_UNIVERSAL_PROMPT_FACTOR_PASSCODE,
@@ -75,10 +76,14 @@
7576
"--username-password-command",
7677
help='Read username and password from the output of a shell command (expected JSON format: `{"username": "myusername", "password": "mypassword"}`)',
7778
)
79+
@click.option(
80+
"--mfa-token-command",
81+
help='Read MFA token from the output of a shell command (expected JSON format: `{"mfa_token": "123654"}`)',
82+
)
7883
@click.option(
7984
'--env',
8085
is_flag=True,
81-
help='Read username, password from environment variables (username and password).',
86+
help='Read username, password and optionally an MFA token from environment variables (username, password and mfa_token).',
8287
)
8388
@click.option(
8489
'--stdin',
@@ -161,6 +166,7 @@ def login(
161166
provider_id,
162167
s3_signature_version,
163168
username_password_command,
169+
mfa_token_command,
164170
env,
165171
stdin,
166172
authfile,
@@ -194,6 +200,7 @@ def login(
194200
session_duration,
195201
sspi,
196202
username_password_command,
203+
mfa_token_command,
197204
duo_factor,
198205
duo_device,
199206
aad_verification_code,
@@ -220,11 +227,9 @@ def login(
220227
# If we fail to get an assertion, prompt for credentials and try again
221228
if assertion is None:
222229
password = None
223-
224230
if config.username_password_command:
225-
config.adfs_user, password = _username_password_command_credentials(
226-
username_password_command
227-
)
231+
data = run_command.run_command(username_password_command)
232+
config.adfs_user, password = data['username'], data['password']
228233
if stdin:
229234
config.adfs_user, password = _stdin_user_credentials()
230235
elif env:
@@ -438,40 +443,6 @@ def _emit_summary(config, session_duration):
438443
err=True
439444
)
440445

441-
442-
def _username_password_command_credentials(username_password_command):
443-
try:
444-
logging.debug("Executing `{}`".format(username_password_command))
445-
proc = subprocess.run(
446-
username_password_command,
447-
stdout=subprocess.PIPE,
448-
stderr=subprocess.STDOUT,
449-
check=True,
450-
shell=True,
451-
)
452-
data = json.loads(proc.stdout)
453-
username = data["username"]
454-
password = data["password"]
455-
except subprocess.CalledProcessError as e:
456-
logging.error(
457-
"Failed to execute the `{}` command to retrieve username and password: \n\n{}".format(
458-
username_password_command, e.output
459-
)
460-
)
461-
username = None
462-
password = None
463-
except json.JSONDecodeError as e:
464-
logging.error(
465-
"Failed to decode the output of the `{}` command as JSON to retrieve username and password: \n\n{}".format(
466-
username_password_command, e
467-
)
468-
)
469-
username = None
470-
password = None
471-
472-
return username, password
473-
474-
475446
def _file_user_credentials(profile, authfile):
476447
config = configparser.ConfigParser()
477448

aws_adfs/prepare.py

+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def get_prepared_config(
2121
session_duration,
2222
sspi,
2323
username_password_command,
24+
mfa_token_command,
2425
duo_factor,
2526
duo_device,
2627
aad_verification_code=None,
@@ -47,6 +48,7 @@ def get_prepared_config(
4748
:param session_duration: AWS STS session duration (default 1 hour)
4849
:param sspi: Whether SSPI is enabled
4950
:param username_password_command: The command used to retrieve username and password information
51+
:param mfa_token_command: The command used to retrieve MFA token
5052
:param duo_factor: The specific Duo factor to use
5153
:param duo_device: The specific Duo device to use
5254
:param aad_verification_code: If verification code is in config use that for multi-factor authentication
@@ -86,6 +88,7 @@ def default_if_none(value, default):
8688
adfs_config.session_duration = default_if_none(session_duration, adfs_config.session_duration)
8789
adfs_config.sspi = default_if_none(sspi, adfs_config.sspi)
8890
adfs_config.username_password_command = default_if_none(username_password_command, adfs_config.username_password_command)
91+
adfs_config.mfa_token_command = default_if_none(mfa_token_command, adfs_config.mfa_token_command)
8992
adfs_config.duo_factor = default_if_none(duo_factor, adfs_config.duo_factor)
9093
adfs_config.duo_device = default_if_none(duo_device, adfs_config.duo_device)
9194
adfs_config.aad_verification_code = aad_verification_code
@@ -149,6 +152,11 @@ def create_adfs_default_config(profile):
149152
# The command used to retrieve username and password information
150153
config.username_password_command = None
151154

155+
# The command used to retrieve MFA token information
156+
config.mfa_token_command = None
157+
158+
config.mfa_token = os.environ.get('mfa_token')
159+
152160
# The specific Duo factor and device to use
153161
config.duo_factor = None
154162
config.duo_device = None
@@ -219,6 +227,7 @@ def load_config(config, profile):
219227
profile, 'adfs_config.sspi',
220228
str(adfs_config.sspi)))
221229
adfs_config.username_password_command = config.get_or(profile, 'adfs_config.username_password_command', adfs_config.username_password_command)
230+
adfs_config.mfa_token_command = config.get_or(profile, 'adfs_config.mfa_token_command', adfs_config.mfa_token_command)
222231

223232
adfs_config.duo_factor = config.get_or(profile, "adfs_config.duo_factor", adfs_config.duo_factor)
224233
if adfs_config.duo_factor == "None":

aws_adfs/run_command.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import json
2+
import logging
3+
import subprocess
4+
5+
def run_command(command):
6+
try:
7+
logging.debug("Executing `{}`".format(command))
8+
proc = subprocess.run(
9+
command,
10+
stdout=subprocess.PIPE,
11+
stderr=subprocess.STDOUT,
12+
check=True,
13+
shell=True,
14+
)
15+
data = json.loads(proc.stdout)
16+
except subprocess.CalledProcessError as e:
17+
logging.error(
18+
"Failed to execute the `{}` command: \n\n{}".format(
19+
command, e.output
20+
)
21+
)
22+
data = None
23+
except json.JSONDecodeError as e:
24+
logging.error(
25+
"Failed to decode the output of the `{}` command as JSON: \n\n{}".format(
26+
command, e
27+
)
28+
)
29+
data = None
30+
31+
return data

test/test_config_preparation.py

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def test_when_there_is_no_profile_use_default_values(self):
2424
default_session_duration = 3600
2525
default_sspi = False
2626
default_username_password_command = None
27+
default_mfa_token_command = None
2728
default_duo_factor = None
2829
default_duo_device = None
2930
default_enforce_role_arn = False
@@ -41,6 +42,7 @@ def test_when_there_is_no_profile_use_default_values(self):
4142
default_session_duration,
4243
default_sspi,
4344
default_username_password_command,
45+
default_mfa_token_command,
4446
default_duo_factor,
4547
default_duo_device,
4648
)
@@ -80,6 +82,7 @@ def test_when_the_profile_exists_but_lacks_ssl_verification_use_default_value(se
8082
irrelevant_s3_signature_version = "irrelevant_s3_signature_version"
8183
irrelevant_session_duration = "irrelevant_session_duration"
8284
irrelevant_username_password_command = "irrelevant_username_password_command"
85+
irrelevant_mfa_token_command = "irrelevant_mfa_token_command"
8386
irrelevant_duo_factor = "irrelevant_duo_factor"
8487
irrelevant_duo_device = "irrelevant_duo_device"
8588

@@ -96,6 +99,7 @@ def test_when_the_profile_exists_but_lacks_ssl_verification_use_default_value(se
9699
irrelevant_session_duration,
97100
default_sspi,
98101
irrelevant_username_password_command,
102+
irrelevant_mfa_token_command,
99103
irrelevant_duo_factor,
100104
irrelevant_duo_device,
101105
)

0 commit comments

Comments
 (0)