Skip to content

Commit

Permalink
Option to remember the MFA device
Browse files Browse the repository at this point in the history
According to the documentation, the MFA device could be remembered for a limited time if user asks for it.

+info:

https://developer.okta.com/docs/api/resources/authn#verify-factor
  • Loading branch information
edubxb committed Feb 17, 2019
1 parent b54a3c3 commit afb1bb7
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ A configuration wizard will prompt you to enter the necessary configuration para
- call - OTP via Voice call
- sms - OTP via SMS message
- resolve_aws_alias - y or n. If yes, gimme-aws-creds will try to resolve AWS account ids with respective alias names (default: n). This option can also be set interactively in the command line using `-r` or `--resolve parameter`
- remember_device - y or n. If yes, the MFA device will be remembered by Okta service for a limited time. This option can also be set interactively in the command line using `-m` or `--remember-device`

## Usage

Expand Down
34 changes: 33 additions & 1 deletion gimme_aws_creds/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import sys
from os.path import expanduser
from urllib.parse import urlparse

from . import version


Expand All @@ -39,6 +40,7 @@ def __init__(self):
self.app_url = None
self.resolve = False
self.mfa_code = None
self.remember_device = False
self.aws_default_duration = 3600
self.device_token = None

Expand Down Expand Up @@ -89,6 +91,12 @@ def get_args(self):
help="The MFA verification code to be used with SMS or TOTP authentication methods. "
"If not provided you will be prompted to enter an MFA verification code."
)
parser.add_argument(
'--remember-device', '-m',
action='store_true',
help="The MFA device will be remembered by Okta service for a limited time, "
"otherwise, you will be prompted for it every time."
)
parser.add_argument(
'--version', action='version',
version='%(prog)s {}'.format(version),
Expand Down Expand Up @@ -116,6 +124,8 @@ def get_args(self):
self.username = args.username
if args.mfa_code is not None:
self.mfa_code = args.mfa_code
if args.remember_device:
self.remember_device = True
if args.resolve is True:
self.resolve = True
self.conf_profile = args.profile or 'DEFAULT'
Expand Down Expand Up @@ -169,9 +179,10 @@ def update_config_file(self):
'write_aws_creds': '',
'cred_profile': 'role',
'okta_username': '',
'app_url': '',
'app_url': '',
'resolve_aws_alias': 'n',
'preferred_mfa_type': '',
'remember_device': 'n',
'aws_default_duration': '3600',
'device_token': ''
}
Expand Down Expand Up @@ -206,6 +217,7 @@ def update_config_file(self):
config_dict['okta_username'] = self._get_okta_username(defaults['okta_username'])
config_dict['aws_default_duration'] = self._get_aws_default_duration(defaults['aws_default_duration'])
config_dict['preferred_mfa_type'] = self._get_preferred_mfa_type(defaults['preferred_mfa_type'])
config_dict['remember_device'] = self._get_remember_device(defaults['remember_device'])

# If write_aws_creds is True get the profile name
if config_dict['write_aws_creds'] is True:
Expand Down Expand Up @@ -410,6 +422,26 @@ def _get_preferred_mfa_type(self, default_entry):
"Preferred MFA Device Type", default_entry)
return okta_username

def _get_remember_device(self, default_entry):
"""Option to remember the MFA device"""
print("Do you want the MFA device be remembered?\n"
"Please answer y or n.")
remember_device = None
while remember_device is not True and remember_device is not False:
default_entry = 'y' if default_entry is True else 'n'
answer = self._get_user_input(
"Remember device", default_entry)
answer = answer.lower()

if answer == 'y':
remember_device = True
elif answer == 'n':
remember_device = False
else:
print("Remember the MFA device must be either y or n.")

return remember_device

@staticmethod
def _get_user_input(message, default=None):
"""formats message to include default and then prompts user for input
Expand Down
13 changes: 10 additions & 3 deletions gimme_aws_creds/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class GimmeAWSCreds(object):
--mfa-code MFA_CODE The MFA verification code to be used with SMS or TOTP
authentication methods. If not provided you will be
prompted to enter an MFA verification code.
--remember-device, -m
The MFA device will be remembered by Okta service for
a limited time, otherwise, you will be prompted for it
every time.
--version gimme-aws-creds version
Config Options:
Expand Down Expand Up @@ -362,7 +366,7 @@ def run(self):
else:
if not conf_dict.get('device_token'):
print('No device token in configuration. Try running --register_device again.', file=sys.stderr)
exit(1)
exit(1)

okta = OktaClient(conf_dict['okta_org_url'], config.verify_ssl_certs, conf_dict['device_token'])
if config.resolve == True:
Expand All @@ -383,12 +387,15 @@ def run(self):
if config.mfa_code is not None:
okta.set_mfa_code(config.mfa_code)

okta.set_remember_device(config.remember_device
or conf_dict['remember_device'])

# AWS Default session duration ....
if conf_dict.get('aws_default_duration'):
config.aws_default_duration = int(conf_dict['aws_default_duration'])
else:
config.aws_default_duration = 3600

# Capture the Device Token and write it to the config file
if config.register_device is True:
auth_result = okta.auth_session()
Expand All @@ -397,7 +404,7 @@ def run(self):
print('Device token saved!', file=sys.stderr)
sys.exit()
else:

# Call the Okta APIs and proces data locally
if conf_dict.get('gimme_creds_server') == 'internal':
# Okta API key is required when calling Okta APIs internally
Expand Down
9 changes: 8 additions & 1 deletion gimme_aws_creds/okta.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def __init__(self, okta_org_url, verify_ssl_certs=True, device_token=None):
self._username = None
self._preferred_mfa_type = None
self._mfa_code = None
self._remember_device = None

self._use_oauth_access_token = False
self._use_oauth_id_token = False
Expand Down Expand Up @@ -84,6 +85,9 @@ def set_preferred_mfa_type(self, preferred_mfa_type):
def set_mfa_code(self, mfa_code):
self._mfa_code = mfa_code

def set_remember_device(self, remember_device):
self._remember_device = remember_device

def use_oauth_access_token(self, val=True):
self._use_oauth_access_token = val

Expand Down Expand Up @@ -323,6 +327,7 @@ def _login_send_sms(self, state_token, factor):
""" Send SMS message for second factor authentication"""
response = self._http_client.post(
factor['_links']['verify']['href'],
params={'rememberDevice': self._remember_device},
json={'stateToken': state_token},
headers=self._get_headers(),
verify=self._verify_ssl_certs
Expand All @@ -340,6 +345,7 @@ def _login_send_call(self, state_token, factor):
""" Send Voice call for second factor authentication"""
response = self._http_client.post(
factor['_links']['verify']['href'],
params={'rememberDevice': self._remember_device},
json={'stateToken': state_token},
headers=self._get_headers(),
verify=self._verify_ssl_certs
Expand All @@ -353,11 +359,11 @@ def _login_send_call(self, state_token, factor):
if 'sessionToken' in response_data:
return {'stateToken': None, 'sessionToken': response_data['sessionToken'], 'apiResponse': response_data}


def _login_send_push(self, state_token, factor):
""" Send 'push' for the Okta Verify mobile app """
response = self._http_client.post(
factor['_links']['verify']['href'],
params={'rememberDevice': self._remember_device},
json={'stateToken': state_token},
headers=self._get_headers(),
verify=self._verify_ssl_certs
Expand Down Expand Up @@ -393,6 +399,7 @@ def _login_input_mfa_challenge(self, state_token, next_url):
pass_code = input()
response = self._http_client.post(
next_url,
params={'rememberDevice': self._remember_device},
json={'stateToken': state_token, 'passCode': pass_code},
headers=self._get_headers(),
verify=self._verify_ssl_certs
Expand Down
18 changes: 15 additions & 3 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,21 @@ def tearDown(self):
"""Run Clean Up"""
self.config.clean_up()

@patch('argparse.ArgumentParser.parse_args',
return_value=argparse.Namespace(username='ann', configure=False, profile=None, insecure=False, resolve=None, mfa_code=None, register_device=False, list_profiles=False))
@patch(
"argparse.ArgumentParser.parse_args",
return_value=argparse.Namespace(
username="ann",
configure=False,
profile=None,
insecure=False,
resolve=None,
mfa_code=None,
register_device=False,
list_profiles=False,
remember_device=False,
),
)
def test_get_args_username(self, mock_arg):
"""Test to make sure username gets returned"""
self.config.get_args()
assert_equals(self.config.username, 'ann')
assert_equals(self.config.username, "ann")

0 comments on commit afb1bb7

Please sign in to comment.