Skip to content

Commit

Permalink
Add Okta Verify with Push Feature (#8)
Browse files Browse the repository at this point in the history
* Add Okta Push feature

* Requires more testing and dynamic switch for factor_type=sms | push

* Add environment variable for Twilio and App

* Update, simplify tree base on factor preference
  • Loading branch information
noinarisak authored May 5, 2020
1 parent 81a528a commit b76b97e
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 456 deletions.
13 changes: 12 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
OKTA_API_TOKEN=_GET_FROM_OKTA
OKTA_ORG_URL=https://{_REPLACE_ME_}.okta.com
OKTA_API_TOKEN=_REPLACE_ME_

TWILIO_ACCOUNT_SID=_REPLACE_ME_
TWILIO_API_KEY=_REPLACE_ME_
TWILIO_API_SECRET=_REPLACE_ME_
TWILIO_AUTH_TOKEN=_REPLACE_ME_

TWILIO_PHONE_SID=_REPLACE_ME_
TWILIO_PHONE_WEBHOOK_URL=https://{_REPLACE_ME_}.ngrok.io/ivr/welcome

APP_CUSTOMER_NAME="VIRGIN MOBILE"
15 changes: 11 additions & 4 deletions backend/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Author: [email protected]
# Date:
# Desc: Base makefile template for Python development
# v1: Init
# Date: April 30, 2020
# Desc: Python/Flask and Twilio development tooling

.ONESHELL:
.SHELL := /usr/bin/bash
Expand Down Expand Up @@ -48,6 +47,14 @@ run: venv ## Run
@source venv/bin/activate; python manage.py runserver

.PHONY: test
test: venv ## Run test
test: venv ## Execute test
@echo "+ $@"
@source venv/bin/activate; nosetests project/test

.PHONY: twilio
twilio: ## Update Twilio Voice WebHook
@echo "+ $@"
@twilio login $(TWILIO_ACCOUNT_SID) --auth-token=$(TWILIO_AUTH_TOKEN) --profile=okta --force
@twilio phone-numbers:update $(TWILIO_PHONE_SID) \
--voice-method=POST \
--voice-url=$(TWILIO_PHONE_WEBHOOK_URL)
3 changes: 1 addition & 2 deletions backend/ivr_phone_tree_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ def configure_app(new_app, config_name='development'):


app = Flask(__name__)
import ivr_phone_tree_python.views_new
# import ivr_phone_tree_python.views
import ivr_phone_tree_python.views # noqa: F401

configure_app(app)
4 changes: 3 additions & 1 deletion backend/ivr_phone_tree_python/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import os

from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
load_dotenv(find_dotenv(filename='.env', raise_error_if_not_found=True), verbose=True)


class DefaultConfig(object):
DEBUG = False
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'
OKTA_API_TOKEN = os.getenv('OKTA_API_TOKEN')
OKTA_ORG_URL = os.getenv('OKTA_ORG_URL')
APP_CUSTOMER_NAME = os.getenv('APP_CUSTOMER_NAME', 'OKTA IVR DEMO')


class DevelopmentConfig(DefaultConfig):
Expand Down
249 changes: 72 additions & 177 deletions backend/ivr_phone_tree_python/util/okta.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,23 @@
import requests
import json
import polling

from ivr_phone_tree_python import app

# //TODO: Refactor the Request alls with Env, Http Headers Template
# //TODO: Create CONST for FACTOR TYPES


class OktaIVR(object):
def __init__(self, phone_number):
self._user_phone_number = phone_number

_user = self.__get_user_by_phone(self._user_phone_number)
self._user_id = _user['id']
self._profile = _user['profile']

def _get_user_by_phone(phone_number):
url = "https://bellca.okta.com/api/v1/users?search=profile.mobilePhone eq \"7734547477\"&limit=25"

headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'SSWS {api_token}'.format(api_token=app.config['OKTA_API_TOKEN']),
}
response = requests.request("GET", url, headers=headers)

return response.json()[0]


def get_user(phone_number):
_user = get_user_by_phone(phone_number)
_factor = get_user_factorid_by_factor_type(user_id=_user['id'], factor_type='sms')
_user_factor_preference_type = _user['profile']['ivrFactorPreference']
_factor = get_user_factorid_by_factor_type(user_id=_user['id'], factor_type=_user_factor_preference_type)
_auth = get_mfa_state_token(username=_user['profile']['login'])

return _user, _factor
return _user, _factor, _auth


def get_user_by_phone(phone_number):
query_search = requests.utils.quote('profile.mobilePhone eq "{}"'.format(phone_number))
url = 'https://bellca.okta.com/api/v1/users?search={}&limit=1'.format(query_search)
query_search = requests.utils.quote('profile.ivrPhone eq "{phone_number}"'.format(phone_number=phone_number))
url = '{org_url}/api/v1/users?search={query}&limit=1'.format(org_url=app.config['OKTA_ORG_URL'],
query=query_search)

print('url:')
print(url)
Expand All @@ -55,8 +35,31 @@ def get_user_by_phone(phone_number):
return response.json()[0]


def get_user_factorid_by_factor_type(user_id, factor_type='sms'):
url = 'https://bellca.okta.com/api/v1/users/{user_id}/factors'.format(user_id=user_id)
def get_user_factorid_by_factor_type(user_id=None, factor_type='sms'):
if user_id is None:
raise ValueError('user_id is None')

factor_type_list = get_user_factors(user_id)

result = [factor for factor in factor_type_list if factor['factorType'] == factor_type]
if not result:
raise Exception(
'Not supported factor type. factor_type="{}"'.format(factor_type)
)
if len(result) > 1:
raise Exception(
'Multiple factor were returned.'
)

return result[0]


def get_user_factors(user_id=None):
if user_id is None:
raise ValueError('user_id is None')

url = '{org_url}/api/v1/users/{user_id}/factors'.format(org_url=app.config['OKTA_ORG_URL'],
user_id=user_id)

headers = {
'Authorization': 'SSWS {api_token}'.format(api_token=app.config['OKTA_API_TOKEN']),
Expand All @@ -67,13 +70,16 @@ def get_user_factorid_by_factor_type(user_id, factor_type='sms'):
response = requests.request("GET", url, headers=headers)

factor_type_list = response.json()
if not factor_type_list:
raise Exception(
'No factors found. user={}'.format(user_id)
)

result = [factor for factor in factor_type_list if factor['factorType'] == 'sms']
return result[0]
return factor_type_list


def get_mfa_state_token(username):
url = "https://bellca.okta.com/api/v1/authn"
url = '{org_url}/api/v1/authn'.format(org_url=app.config['OKTA_ORG_URL'])

payload = {
'username': username,
Expand All @@ -88,7 +94,8 @@ def get_mfa_state_token(username):


def send_mfa_challenge(factor_id, state_token):
url = 'https://bellca.okta.com/api/v1/authn/factors/{factor_id}/verify'.format(factor_id=factor_id)
url = '{org_url}/api/v1/authn/factors/{factor_id}/verify'.format(org_url=app.config['OKTA_ORG_URL'],
factor_id=factor_id)

payload = {
'stateToken': state_token
Expand All @@ -104,7 +111,8 @@ def send_mfa_challenge(factor_id, state_token):


def sms_mfa_verify(factor_id, state_token, pass_code):
url = 'https://bellca.okta.com/api/v1/authn/factors/{factor_id}/verify'.format(factor_id=factor_id)
url = '{org_url}/api/v1/authn/factors/{factor_id}/verify'.format(org_url=app.config['OKTA_ORG_URL'],
factor_id=factor_id)

payload = {
'stateToken': state_token,
Expand All @@ -125,17 +133,26 @@ def sms_mfa_verify(factor_id, state_token, pass_code):
return _sms_verified


def push_mfa_polling():
_push_verified = False
def push_mfa_verify(response):
print('push_mfa_verify')
_valid = False
result = response.json()

print('result')
print(result)

if 'status' in result:
if result['status'] == 'SUCCESS':
_valid = True

return _push_verified
return _valid


def push_mfa_verify(factor_id, state_token):
# url = "https://bellca.okta.com/api/v1/authn/factors/opfai42xiNpKst1Qu4x6/verify"
url = 'https://bellca.okta.com/api/v1/authn/factors/{factor_id}}/verify'.format(factor_id=factor_id)
def push_mfa_polling(factor_id, state_token):
_valid = False
url = '{org_url}/api/v1/authn/factors/{factor_id}/verify'.format(org_url=app.config['OKTA_ORG_URL'],
factor_id=factor_id)

# payload = "{\n \"stateToken\": \"0063G-bqEfV6k5JNw35Jc_TnfVAACbs5y-iETHdV4s\",\n \"factorType\": \"push\",\n \"provider\": \"OKTA\"\n}"
payload = {
'stateToken': state_token,
}
Expand All @@ -144,138 +161,16 @@ def push_mfa_verify(factor_id, state_token):
'Content-Type': 'application/json',
}

response = requests.request("POST", url, headers=headers, data=json.dumps(payload))

print(response.text.encode('utf8'))

_push_verified = False
result = response.json()

result = {
"stateToken": "00qwBwEKCHQ-mRbB5N0zxmaq8LrLRMIhwTctfMjSJI",
"expiresAt": "2020-04-26T18:15:10.000Z",
"status": "MFA_CHALLENGE",
"factorResult": "WAITING",
"challengeType": "FACTOR",
"_embedded": {
"user": {
"id": "00uaezbs2mhikusii4x6",
"passwordChanged": "2020-04-24T21:05:48.000Z",
"profile": {
"login": "[email protected]",
"firstName": "Noi",
"lastName": "Narisak",
"locale": "en",
"timeZone": "America/Los_Angeles"
}
},
"factor": {
"id": "opfai42xiNpKst1Qu4x6",
"factorType": "push",
"provider": "OKTA",
"vendorName": "OKTA",
"profile": {
"credentialId": "[email protected]",
"deviceType": "SmartPhone_Android",
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "default",
"e": "AQAB",
"n": "r75TcqG2gEIrBL6COX8tM9PyJZ4Qeo8w8Y3GTpg1p0OgpX24aBmqjM_QrUVzFklNwaahBgY5hKrO6amuTeMxJKo-PySX4RY0CpMrleBG7RfauVBF_OWZ-5Goz3Kb5h34lD4IjZkJMvHHlM8Q0ib2Vu2H2kBPmBUo4bmavldOUTIS62pPQRduEzojnUdgbhUoJ30vhhDw5aPmpscguJdf5CoXQRim5OcdoO6iu5Wbr29_edEX3b1VEDrmkj42HW4O-rdLuApIVn6oKrp4rL4XWr8YG9KnIX_lxVo_DTj69ayIKpvUF8wF75_DxRuBRr-V7VhYBoh-RQUn1OLIWlF1qQ" # noqa: E501
}
],
"name": "Pixel 3",
"platform": "ANDROID",
"version": "29"
}
},
"policy": {
"allowRememberDevice": False,
"rememberDeviceLifetimeInMinutes": 0,
"rememberDeviceByDefault": False,
"factorsPolicyInfo": {
"opfai42xiNpKst1Qu4x6": {
"autoPushEnabled": False
}
}
}
},
"_links": {
"next": {
"name": "poll",
"href": "https://bellca.okta.com/api/v1/authn/factors/opfai42xiNpKst1Qu4x6/verify",
"hints": {
"allow": [
"POST"
]
}
},
"resend": [
{
"name": "push",
"href": "https://bellca.okta.com/api/v1/authn/factors/opfai42xiNpKst1Qu4x6/verify/resend",
"hints": {
"allow": [
"POST"
]
}
}
],
"prev": {
"href": "https://bellca.okta.com/api/v1/authn/previous",
"hints": {
"allow": [
"POST"
]
}
},
"cancel": {
"href": "https://bellca.okta.com/api/v1/authn/cancel",
"hints": {
"allow": [
"POST"
]
}
}
}
}

# //TODO: Logic
# Loop interval every 5 seconds for 30 seconds. //NOTE: Look into https://github.com/justiniso/polling
# if 'factorResult' in result: // factorResult attibute means we still waiting
# POST "https://bellca.okta.com/api/v1/authn/factors/opfai42xiNpKst1Qu4x6/verify"
#
#

result = {
"expiresAt": "2020-04-26T18:12:20.000Z",
"status": "SUCCESS",
"sessionToken": "20111e1RJIsxLZv1-5KCmzKi1G1I2XOlv1HHmHEDNBMHD_d1vaqwDp5",
"_embedded": {
"user": {
"id": "00uaezbs2mhikusii4x6",
"passwordChanged": "2020-04-24T21:05:48.000Z",
"profile": {
"login": "[email protected]",
"firstName": "Noi",
"lastName": "Narisak",
"locale": "en",
"timeZone": "America/Los_Angeles"
}
}
},
"_links": {
"cancel": {
"href": "https://bellca.okta.com/api/v1/authn/cancel",
"hints": {
"allow": [
"POST"
]
}
}
}
}

return result
try:
polling.poll(
lambda: requests.request("POST", url, headers=headers, data=json.dumps(payload)),
check_success=push_mfa_verify,
step=2, # Attempt 3 times.
timeout=10 # Timeout after 10 seconds.
)
_valid = True
except polling.TimeoutException as te:
while not te.values.empty():
print(te.values.get())

return _valid
1 change: 0 additions & 1 deletion backend/ivr_phone_tree_python/util/okta_cls.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ def push_mfa_verify(factor_id, state_token):

print(response.text.encode('utf8'))

_push_verified = False
result = response.json()

result = {
Expand Down
Loading

0 comments on commit b76b97e

Please sign in to comment.