Skip to content

Commit

Permalink
Merge pull request #406 from elishagreenwald/add-email-2-factor
Browse files Browse the repository at this point in the history
feat: add email option for mfa
  • Loading branch information
epierce authored Jun 1, 2023
2 parents 4f37919 + d319dc7 commit 1019a08
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ A configuration wizard will prompt you to enter the necessary configuration para
- token:hardware - OTP using hardware like Yubikey
- call - OTP via Voice call
- sms - OTP via SMS message
- email - OTP via email
- web - DUO uses localhost webbrowser to support push|call|passcode
- passcode - DUO uses `OKTA_MFA_CODE` or `--mfa-code` if set, or prompts user for passcode(OTP).
Expand Down
1 change: 1 addition & 0 deletions gimme_aws_creds/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ def _get_preferred_mfa_type(self, default_entry):
- token:hardware - OTP using hardware like Yubikey
- call - OTP via Voice call
- sms - OTP via SMS message
- email - OTP via email message
- web - DUO uses localhost webbrowser to support push|call|passcode
- passcode - DUO uses `OKTA_MFA_CODE` or `--mfa-code` if set, or prompts user for passcode(OTP).
"""
Expand Down
23 changes: 23 additions & 0 deletions gimme_aws_creds/okta_classic.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,25 @@ def _login_send_sms(self, state_token, factor):
if 'sessionToken' in response_data:
return {'stateToken': None, 'sessionToken': response_data['sessionToken'], 'apiResponse': response_data}

def _login_send_email(self, state_token, factor):
""" Send email 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
)
response.raise_for_status()

self.ui.info("A verification code has been sent to " + factor['profile']['email'])
response_data = response.json()

if 'stateToken' in response_data:
return {'stateToken': response_data['stateToken'], 'apiResponse': response_data}
if 'sessionToken' in response_data:
return {'stateToken': None, 'sessionToken': response_data['sessionToken'], 'apiResponse': response_data}

def _login_send_call(self, state_token, factor):
""" Send Voice call for second factor authentication"""
response = self._http_client.post(
Expand Down Expand Up @@ -554,6 +573,8 @@ def _login_multi_factor(self, state_token, login_data):
return self._login_send_sms(state_token, factor)
elif factor['factorType'] == 'call':
return self._login_send_call(state_token, factor)
elif factor['factorType'] == 'email':
return self._login_send_email(state_token, factor)
elif factor['factorType'] == 'token:software:totp':
return self._login_input_mfa_challenge(state_token, factor['_links']['verify']['href'])
elif factor['factorType'] == 'token':
Expand Down Expand Up @@ -834,6 +855,8 @@ def _build_factor_name(self, factor):
return "Okta Verify App: " + factor['profile']['deviceType'] + ": " + factor['profile']['name']
elif factor['factorType'] == 'sms':
return factor['factorType'] + ": " + factor['profile']['phoneNumber']
elif factor['factorType'] == 'email':
return factor['factorType'] + ": " + factor['profile']['email']
elif factor['factorType'] == 'call':
return factor['factorType'] + ": " + factor['profile']['phoneNumber']
elif factor['factorType'] == 'token:software:totp':
Expand Down
124 changes: 123 additions & 1 deletion tests/test_okta_classic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ def setUp(self):
}
}

self.email_factor = {
"id": "ema9hmdk2qvhjOQQ30h7",
"factorType": "email",
"provider": "OKTA",
"vendorName": "OKTA",
"profile": {
"email": "[email protected]"
},
"_links": {
"verify": {
"href": "https://example.okta.com/api/v1/authn/factors/ema9hmdk2qvhjOQQ30h7/verify",
"hints": {
"allow": [
"POST"
]
}
}
}
}

self.push_factor = {
"id": "opf9ei43pbAgb2qgc0h7",
"factorType": "push",
Expand Down Expand Up @@ -194,7 +214,7 @@ def setUp(self):
</html>"""

self.factor_list = [self.sms_factor, self.push_factor, self.totp_factor, self.webauthn_factor]
self.factor_list = [self.sms_factor, self.push_factor, self.totp_factor, self.webauthn_factor, self.email_factor]

def setUp_client(self, okta_org_url, verify_ssl_certs):
client = OktaClassicClient(ui.default, okta_org_url, verify_ssl_certs)
Expand Down Expand Up @@ -423,6 +443,97 @@ def test_login_send_sms(self):
result = self.client._login_send_sms(self.state_token, self.sms_factor)
self.assertEqual(result, {'stateToken': self.state_token, 'apiResponse': verify_response})

@responses.activate
def test_login_send_email(self):
"""Test that email messages can be requested for MFA"""

verify_response = {
"stateToken": "00Wf8xZJ79mSoTYnJqXbvRegT8QB1EX1IBVk1TU7KI",
"type": "SESSION_STEP_UP",
"expiresAt": "2017-06-15T15:06:10.000Z",
"status": "MFA_CHALLENGE",
"_embedded": {
"user": {
"id": "00u8cakq7vQwtK7sR0h7",
"profile": {
"login": "[email protected]",
"firstName": "Jane",
"lastName": "Doe",
"locale": "en",
"timeZone": "America/Los_Angeles"
}
},
"factor": {
"id": "ema9hmdk2qvhjOQQ30h7",
"factorType": "email",
"provider": "OKTA",
"vendorName": "OKTA",
"profile": {
"email": "[email protected]"
}
},
"policy": {
"allowRememberDevice": False,
"rememberDeviceLifetimeInMinutes": 0,
"rememberDeviceByDefault": False
},
"target": {
"type": "APP",
"name": "gimmecredsserver",
"label": "Gimme-Creds-Server (Dev)",
"_links": {
"logo": {
"name": "medium",
"href": "https://op1static.oktacdn.com/bc/globalFileStoreRecord?id=gfsatgifysE8NG37F0h7",
"type": "image/png"
}
}
}
},
"_links": {
"next": {
"name": "verify",
"href": "https://example.okta.com/api/v1/authn/factors/ema9hmdk2qvhjOQQ30h7/verify",
"hints": {
"allow": [
"POST"
]
}
},
"cancel": {
"href": "https://example.okta.com/api/v1/authn/cancel",
"hints": {
"allow": [
"POST"
]
}
},
"prev": {
"href": "https://example.okta.com/api/v1/authn/previous",
"hints": {
"allow": [
"POST"
]
}
},
"resend": [
{
"name": "sms",
"href": "https://example.okta.com/api/v1/authn/factors/ema9hmdk2qvhjOQQ30h7/verify/resend",
"hints": {
"allow": [
"POST"
]
}
}
]
}
}

responses.add(responses.POST, 'https://example.okta.com/api/v1/authn/factors/ema9hmdk2qvhjOQQ30h7/verify', status=200, body=json.dumps(verify_response))
result = self.client._login_send_email(self.state_token, self.email_factor)
self.assertEqual(result, {'stateToken': self.state_token, 'apiResponse': verify_response})

@responses.activate
def test_login_send_push(self):
"""Test that Okta Verify can be used for MFA"""
Expand Down Expand Up @@ -996,6 +1107,12 @@ def test_choose_factor_totp(self, mock_input):
result = self.client._choose_factor(self.factor_list)
self.assertEqual(result, self.totp_factor)

@patch('builtins.input', return_value='4')
def test_choose_factor_totp(self, mock_input):
""" Test selecting email as a MFA"""
result = self.client._choose_factor(self.factor_list)
self.assertEqual(result, self.email_factor)

@patch('builtins.input', return_value='12')
def test_choose_bad_factor_totp(self, mock_input):
""" Test selecting an invalid MFA factor"""
Expand All @@ -1019,6 +1136,11 @@ def test_build_factor_name_sms(self):
result = self.client._build_factor_name(self.sms_factor)
self.assertEqual(result, "sms: +1 XXX-XXX-1234")

def test_build_factor_name_email(self):
""" Test building a display name for email"""
result = self.client._build_factor_name(self.email_factor)
self.assertEqual(result, "email: [email protected]")

def test_build_factor_name_push(self):
""" Test building a display name for push"""
result = self.client._build_factor_name(self.push_factor)
Expand Down

0 comments on commit 1019a08

Please sign in to comment.