@@ -17,6 +17,8 @@ from frappe.translate import get_lang_code | |||||
from frappe.utils.password import check_password | from frappe.utils.password import check_password | ||||
from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log | from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log | ||||
from erpnext.setup.doctype.sms_settings.sms_settings import send_request | |||||
from urllib import quote | from urllib import quote | ||||
import pyotp | import pyotp | ||||
@@ -127,25 +129,90 @@ class LoginManager: | |||||
if not otp: | if not otp: | ||||
self.authenticate() | self.authenticate() | ||||
# after authenticate, self.user is set (from check_password() call) | # after authenticate, self.user is set (from check_password() call) | ||||
user_info = frappe.db.get_value('User', self.user, ['two_factor_auth','two_factor_setup'], as_dict=1) | |||||
if user_info.two_factor_auth == 1: | |||||
if user_info.two_factor_setup: | |||||
frappe.local.response['verification'] = {'setup_completed':True} | |||||
otp_secret = frappe.db.get_default(self.user + '_otpsecret') | |||||
user_obj = frappe.get_doc('User', self.user) | |||||
two_factor_auth_user = len(frappe.db.sql("""select name from `tabRole` where two_factor_auth=1 | |||||
and name in ({0}) limit 1""".format(', '.join(['%s'] * len(user_obj.roles))), | |||||
[d.role for d in user_obj.roles])) | |||||
if two_factor_auth_user == 1: | |||||
otp_secret = frappe.db.get_default(self.user + '_otpsecret') | |||||
restrict_method = frappe.db.get_value('System Settings', None, 'fix_2fa_method') | |||||
verification_meth = frappe.db.get_value('User', self.user, 'two_factor_method') | |||||
if restrict_method: | |||||
try: | |||||
fixed_method = frappe.db.sql('''SELECT DEFAULT(two_factor_method) AS 'default_method' FROM | |||||
(SELECT 1) AS dummy LEFT JOIN tabUser on True LIMIT 1;''', as_dict=1) | |||||
except OperationalError: | |||||
fixed_method = [frappe._dict()] | |||||
if not verification_meth: | |||||
verification_method = fixed_method[0].default_method or 'OTP App' | |||||
else: | |||||
verification_method = fixed_method[0].default_method or verification_meth | |||||
if otp_secret: | |||||
token = int(pyotp.TOTP(otp_secret).now()) | |||||
if verification_method == 'SMS': | |||||
user_phone = frappe.db.get_value('User', self.user, ['phone','mobile_no'], as_dict=1) | |||||
usr_phone = user_phone.mobile_no or user_phone.phone | |||||
status = self.send_token_via_sms(token=token, phone_no=usr_phone, otpsecret=otp_secret) | |||||
verification_obj = {'token_delivery': status, | |||||
'prompt': status and 'Enter verification code sent to {}'.format(usr_phone[:4] + '******' + usr_phone[-3:]), | |||||
'method': 'SMS'} | |||||
elif verification_method == 'OTP App': | |||||
totp_uri = False | |||||
if frappe.db.get_default(self.user + '_otpsecret', otp_secret): | |||||
totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") | |||||
verification_obj = {'token_delivery': True, | |||||
'prompt': False, | |||||
'totp_uri': totp_uri, | |||||
'method': 'OTP App'} | |||||
elif verification_method == 'Email': | |||||
status = self.send_token_via_email(token=token,otpsecret=otp_secret) | |||||
verification_obj = {'token_delivery': status, | |||||
'prompt': status and 'Enter verification code sent to your registered email address', | |||||
'method': 'Email'} | |||||
frappe.local.response['verification'] = verification_obj | |||||
else: | else: | ||||
import os | import os | ||||
import base64 | import base64 | ||||
otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') | otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') | ||||
frappe.db.set_default(self.user + '_otpsecret', otp_secret) | |||||
frappe.db.commit() | |||||
totp_uri = pyotp.totp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") | totp_uri = pyotp.totp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") | ||||
frappe.local.response['verification'] = {'setup_completed':False, 'totp_uri':totp_uri} | |||||
# not actual token but counter ( hotp.at(counter) gives token) | |||||
token = int(pyotp.TOTP(otp_secret).now()) | |||||
frappe.local.response['verification'] = { | |||||
'method_first_time': True, | |||||
'token_delivery': True, | |||||
'prompt': False, | |||||
'totp_uri': totp_uri, | |||||
'restrict_method': fixed_method[0].default_method or 'OTP App' | |||||
} | |||||
tmp_id = frappe.generate_hash(length=8) | tmp_id = frappe.generate_hash(length=8) | ||||
usr = frappe.form_dict.get('usr') | usr = frappe.form_dict.get('usr') | ||||
pwd = frappe.form_dict.get('pwd') | pwd = frappe.form_dict.get('pwd') | ||||
frappe.cache().hset('token',tmp_id,{'usr':usr,'pwd':pwd,'otp_secret':otp_secret}) | |||||
if verification_method in ['SMS', 'Email']: | |||||
frappe.cache().set(tmp_id + '_token',token) | |||||
frappe.cache().expire(tmp_id + '_token',300) | |||||
frappe.cache().set(tmp_id + '_usr', usr) | |||||
frappe.cache().set(tmp_id + '_pwd', pwd) | |||||
frappe.cache().set(tmp_id + '_otp_secret', otp_secret) | |||||
frappe.cache().set(tmp_id + '_user', self.user) | |||||
for field in [tmp_id + nm for nm in ['_usr', '_pwd', '_otp_secret', '_user']]: | |||||
frappe.cache().expire(field,120) | |||||
frappe.local.response['tmp_id'] = tmp_id | frappe.local.response['tmp_id'] = tmp_id | ||||
raise frappe.RequestToken | raise frappe.RequestToken | ||||
@@ -155,13 +222,14 @@ class LoginManager: | |||||
else: | else: | ||||
try: | try: | ||||
tmp_info = frappe.cache().hget('token', frappe.form_dict.get('tmp_id')) | |||||
tmp_info = { | |||||
'usr': frappe.cache().get(frappe.form_dict.get('tmp_id')+'_usr'), | |||||
'pwd': frappe.cache().get(frappe.form_dict.get('tmp_id')+'_pwd') | |||||
} | |||||
self.authenticate(user=tmp_info['usr'], pwd=tmp_info['pwd']) | self.authenticate(user=tmp_info['usr'], pwd=tmp_info['pwd']) | ||||
except: | except: | ||||
frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") | frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") | ||||
#frappe.respond_as_web_page("Logged Out", """<p>You have been logged out.</p><p><a href='index'>Back to Home</a></p>""") | |||||
#frappe.throw("+++++ YOUR LOGIN WAS SUCCESSFUL, CONGRATS +++++") | |||||
#frappe.website.render('/404.html') | |||||
self.post_login() | self.post_login() | ||||
def post_login(self,no_two_auth=False): | def post_login(self,no_two_auth=False): | ||||
@@ -169,27 +237,42 @@ class LoginManager: | |||||
self.validate_ip_address() | self.validate_ip_address() | ||||
self.validate_hour() | self.validate_hour() | ||||
if frappe.form_dict.get('otp') and not no_two_auth: | if frappe.form_dict.get('otp') and not no_two_auth: | ||||
self.confirm_token(otp=frappe.form_dict.get('otp'), tmp_id=frappe.form_dict.get('tmp_id')) | |||||
hotp_token = frappe.cache().get(frappe.form_dict.get('tmp_id') + '_token') | |||||
self.confirm_token(otp=frappe.form_dict.get('otp'), tmp_id=frappe.form_dict.get('tmp_id'), hotp_token=hotp_token) | |||||
self.make_session() | self.make_session() | ||||
self.set_user_info() | self.set_user_info() | ||||
else: | else: | ||||
self.make_session() | self.make_session() | ||||
self.set_user_info() | self.set_user_info() | ||||
def confirm_token(self,otp=None, tmp_id=None): | |||||
def confirm_token(self,otp=None, tmp_id=None, hotp_token=False): | |||||
try: | try: | ||||
otp_secret = frappe.cache().hget('token',tmp_id).get('otp_secret') | |||||
otp_secret = frappe.cache().get(tmp_id + '_otp_secret') or frappe.db.get_default(self.user + '_otpsecret') | |||||
if not otp_secret: | |||||
return False | |||||
except AttributeError: | except AttributeError: | ||||
return False | return False | ||||
if hotp_token: | |||||
u_hotp = pyotp.HOTP(otp_secret) | |||||
if u_hotp.verify(otp, int(hotp_token)): | |||||
if not frappe.db.get_default(self.user + '_otpsecret'): | |||||
frappe.db.set_default(self.user + '_otpsecret', otp_secret) | |||||
frappe.cache().delete(tmp_id + '_token') | |||||
return True | |||||
else: | |||||
self.fail('Incorrect Verification code', self.user) | |||||
totp = pyotp.TOTP(otp_secret) | totp = pyotp.TOTP(otp_secret) | ||||
if totp.verify(otp): | if totp.verify(otp): | ||||
frappe.cache().hdel('token', tmp_id) | |||||
# show qr code only once | # show qr code only once | ||||
frappe.db.set_value("User", self.user, 'two_factor_setup', 1) | |||||
frappe.db.commit() | |||||
if not frappe.db.get_default(self.user + '_otpsecret'): | |||||
frappe.db.set_default(self.user + '_otpsecret', otp_secret) | |||||
frappe.db.set_default(self.user + '_otplogin', 1) | |||||
return True | return True | ||||
else: | else: | ||||
self.fail('Incorrect Verification code', user=frappe.cache().hget('token',tmp_id).get('usr')) | |||||
self.fail('Incorrect Verification code', self.user) | |||||
def set_user_info(self, resume=False): | def set_user_info(self, resume=False): | ||||
# set sid again | # set sid again | ||||
@@ -333,6 +416,35 @@ class LoginManager: | |||||
def clear_cookies(self): | def clear_cookies(self): | ||||
clear_cookies() | clear_cookies() | ||||
def send_token_via_sms(self,otpsecret,token=None,phone_no=None): | |||||
ss = frappe.get_doc('SMS Settings', 'SMS Settings') | |||||
if not ss.sms_gateway_url: | |||||
return False | |||||
hotp = pyotp.HOTP(otpsecret) | |||||
args = {ss.message_parameter: 'verification code is {}'.format(hotp.at(int(token)))} | |||||
for d in ss.get("parameters"): | |||||
args[d.parameter] = d.value | |||||
if not phone_no: | |||||
return False | |||||
args[ss.receiver_parameter] = phone_no | |||||
status = send_request(ss.sms_gateway_url, args) | |||||
if 200 <= status < 300: | |||||
return True | |||||
else: | |||||
return False | |||||
def send_token_via_email(self,token,otpsecret): | |||||
user_email = frappe.db.get_value('User',self.user, 'email') | |||||
if not user_email: | |||||
return False | |||||
hotp = pyotp.HOTP(otpsecret) | |||||
frappe.sendmail(recipients=user_email, sender=None, subject='Verification Code', | |||||
message='<p>Your verification code is {}</p>'.format(hotp.at(int(token))),delayed=False, retry=3) | |||||
return True | |||||
class CookieManager: | class CookieManager: | ||||
def __init__(self): | def __init__(self): | ||||
self.cookies = {} | self.cookies = {} | ||||
@@ -367,6 +479,7 @@ class CookieManager: | |||||
for key in set(self.to_delete): | for key in set(self.to_delete): | ||||
response.set_cookie(key, "", expires=expires) | response.set_cookie(key, "", expires=expires) | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_logged_user(): | def get_logged_user(): | ||||
return frappe.session.user | return frappe.session.user | ||||
@@ -121,7 +121,7 @@ | |||||
"in_global_search": 0, | "in_global_search": 0, | ||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"in_standard_filter": 0, | "in_standard_filter": 0, | ||||
"label": "Two Factor Authenticaction", | |||||
"label": "Two Factor Authentication", | |||||
"length": 0, | "length": 0, | ||||
"no_copy": 0, | "no_copy": 0, | ||||
"permlevel": 0, | "permlevel": 0, | ||||
@@ -179,7 +179,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-06-28 13:29:49.915545", | |||||
"modified": "2017-07-06 12:42:57.097914", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "Role", | "name": "Role", | ||||
@@ -710,6 +710,39 @@ | |||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"unique": 0 | "unique": 0 | ||||
}, | }, | ||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "0", | |||||
"depends_on": "eval:doc.enable_two_factor_auth==1", | |||||
"description": "If this is checked, the default 2FA method in User > two_factor_method will be used", | |||||
"fieldname": "fix_2fa_method", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Fix authentication method", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | { | ||||
"allow_bulk_edit": 0, | "allow_bulk_edit": 0, | ||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
@@ -996,7 +1029,7 @@ | |||||
"issingle": 1, | "issingle": 1, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-06-29 18:01:46.292635", | |||||
"modified": "2017-07-06 14:44:04.601775", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "System Settings", | "name": "System Settings", | ||||
@@ -107,6 +107,28 @@ frappe.ui.form.on('User', { | |||||
} | } | ||||
cur_frm.dirty(); | cur_frm.dirty(); | ||||
} | } | ||||
frappe.call({ | |||||
method: "get_2fa_params", | |||||
doc:frm.doc, | |||||
callback: function(r) { | |||||
if (r.message){ | |||||
frm.toggle_display('two_factor_method', r.message.show_method_field == true); | |||||
if (r.message.restrict_method){ | |||||
$("select[data-fieldname=two_factor_method] > option").each(function() { | |||||
if ($(this).val() != r.message.restrict_method){ | |||||
$(this).attr('disabled',''); | |||||
} else { | |||||
$(this).removeAttr('disabled') | |||||
} | |||||
}); | |||||
//frm.set_df_property('two_factor_method', 'options', [r.message.restrict_method]); | |||||
//frm.set_value('two_factor_method',r.message.restrict_method) | |||||
//frm.refresh_field('two_factor_method'); | |||||
} | |||||
} | |||||
} | |||||
}); | |||||
}, | }, | ||||
validate: function(frm) { | validate: function(frm) { | ||||
if(frm.roles_editor) { | if(frm.roles_editor) { | ||||
@@ -1729,9 +1729,10 @@ | |||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"columns": 0, | "columns": 0, | ||||
"default": "OTP App", | |||||
"fieldname": "two_factor_method", | "fieldname": "two_factor_method", | ||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
"hidden": 1, | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"ignore_xss_filter": 0, | "ignore_xss_filter": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
@@ -1741,7 +1742,7 @@ | |||||
"label": "Two Factor Authentication Method", | "label": "Two Factor Authentication Method", | ||||
"length": 0, | "length": 0, | ||||
"no_copy": 0, | "no_copy": 0, | ||||
"options": "OTP App\nSMS", | |||||
"options": "OTP App\nSMS\nEmail", | |||||
"permlevel": 0, | "permlevel": 0, | ||||
"precision": "", | "precision": "", | ||||
"print_hide": 0, | "print_hide": 0, | ||||
@@ -1987,68 +1988,6 @@ | |||||
"search_index": 0, | "search_index": 0, | ||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"unique": 0 | "unique": 0 | ||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "0", | |||||
"fieldname": "two_factor_auth", | |||||
"fieldtype": "Check", | |||||
"hidden": 1, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Two Factor Authentication", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "0", | |||||
"fieldname": "two_factor_setup", | |||||
"fieldtype": "Check", | |||||
"hidden": 1, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Two Factor Initial Setup Completed", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
} | } | ||||
], | ], | ||||
"has_web_view": 0, | "has_web_view": 0, | ||||
@@ -2064,7 +2003,7 @@ | |||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 5, | "max_attachments": 5, | ||||
"menu_index": 0, | "menu_index": 0, | ||||
"modified": "2017-06-30 16:26:06.481438", | |||||
"modified": "2017-07-04 15:53:25.877843", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "User", | "name": "User", | ||||
@@ -489,6 +489,38 @@ class User(Document): | |||||
if len(email_accounts) != len(set(email_accounts)): | if len(email_accounts) != len(set(email_accounts)): | ||||
frappe.throw(_("Email Account added multiple times")) | frappe.throw(_("Email Account added multiple times")) | ||||
def get_2fa_params(self, twoFA_method=None,user=None): | |||||
show_method_field = frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') == unicode(1) | |||||
try: | |||||
two_factor_auth_user = len(frappe.db.sql("""select name from `tabRole` where two_factor_auth=1 | |||||
and name in ({0}) limit 1""".format(', '.join(['%s'] * len(self.roles))), | |||||
[d.role for d in self.roles])) | |||||
except Exception as e: | |||||
return {'show_method_field' : False} | |||||
restrict_method = frappe.db.get_value('System Settings', None, 'fix_2fa_method') | |||||
if int(restrict_method): | |||||
try: | |||||
a = frappe.db.sql('''SELECT DEFAULT(two_factor_method) AS 'default_method' FROM | |||||
(SELECT 1) AS dummy LEFT JOIN tabUser on True LIMIT 1;''', as_dict=1) | |||||
restrict_method = a[0].default_method | |||||
except OperationalError: | |||||
a = [frappe._dict()] | |||||
restrict_method = False | |||||
else: | |||||
restrict_method = False | |||||
return {'show_method_field' : (two_factor_auth_user == 1) and show_method_field, 'restrict_method': restrict_method} | |||||
#if not twoFA_method: | |||||
#else: | |||||
# if twoFA_method == 'Email': | |||||
# if not self.email: | |||||
# frappe.throw(_('No User Email Found')) | |||||
# elif twoFA_method == 'SMS': | |||||
# #user_no = frappe.db.get_values('User', user, ['mobile_no', 'phone'], as_dict=1) | |||||
# if not (self.phone or self.mobile_no): | |||||
# frappe.throw(_('No User Phone Number Found')) | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def get_timezones(): | def get_timezones(): | ||||
import pytz | import pytz | ||||
@@ -903,4 +935,69 @@ def handle_password_test_fail(result): | |||||
def update_gravatar(name): | def update_gravatar(name): | ||||
gravatar = has_gravatar(name) | gravatar = has_gravatar(name) | ||||
if gravatar: | if gravatar: | ||||
frappe.db.set_value('User', name, 'user_image', gravatar) | |||||
frappe.db.set_value('User', name, 'user_image', gravatar) | |||||
@frappe.whitelist(allow_guest=True) | |||||
def send_token_via_sms(tmp_id,phone_no=None,user=None): | |||||
from erpnext.setup.doctype.sms_settings.sms_settings import send_request | |||||
if not frappe.cache().ttl(tmp_id + '_token'): | |||||
return False | |||||
token = frappe.cache().get(tmp_id + '_token') | |||||
ss = frappe.get_doc('SMS Settings', 'SMS Settings') | |||||
if not ss.sms_gateway_url: | |||||
return False | |||||
args = {ss.message_parameter: 'verification code is {}'.format(token)} | |||||
for d in ss.get("parameters"): | |||||
args[d.parameter] = d.value | |||||
if user: | |||||
user_phone = frappe.db.get_value('User', user, ['phone','mobile_no'], as_dict=1) | |||||
usr_phone = user_phone.mobile_no or user_phone.phone | |||||
if not usr_phone: | |||||
return False | |||||
else: | |||||
if phone_no: | |||||
usr_phone = phone_no | |||||
else: | |||||
return False | |||||
args[ss.receiver_parameter] = usr_phone | |||||
status = send_request(ss.sms_gateway_url, args) | |||||
if 200 <= status < 300: | |||||
frappe.cache().delete(tmp_id + '_token') | |||||
return True | |||||
else: | |||||
return False | |||||
@frappe.whitelist(allow_guest=True) | |||||
def send_token_via_email(tmp_id,token=None): | |||||
import pyotp | |||||
user = frappe.cache().get(tmp_id + '_user') | |||||
count = token or frappe.cache().get(tmp_id + '_token') | |||||
if ((not user) or (user == 'None') or (not count)): | |||||
return False | |||||
otpsecret = frappe.cache().get(tmp_id + '_otp_secret') | |||||
hotp = pyotp.HOTP(otpsecret) | |||||
user_email = frappe.db.get_value('User',user, 'email') | |||||
if not user_email: | |||||
return False | |||||
frappe.sendmail(recipients=user_email, sender=None, subject='Verification Code', | |||||
message='<p>Your verification code is {0}</p>'.format(hotp.at(int(count))),delayed=False, retry=3) | |||||
return True | |||||
@frappe.whitelist(allow_guest=True) | |||||
def set_verification_method(tmp_id,method=None): | |||||
user = frappe.cache().get(tmp_id + '_user') | |||||
if ((not user) or (user == 'None') or (not method)): | |||||
return False | |||||
frappe.db.set_value('User', user, 'two_factor_method', method) | |||||
frappe.db.commit() |
@@ -159,37 +159,211 @@ login.login_handlers = (function() { | |||||
console.log(data); | console.log(data); | ||||
if(data.verification) { | if(data.verification) { | ||||
login.set_indicator("{{ _("Success") }}", 'green'); | login.set_indicator("{{ _("Success") }}", 'green'); | ||||
$('.login-content').empty().append($('<div>').attr({'id':'otp_div'}).html('<form class="form-verify"><div class="page-card-head">\ | |||||
var continue_otp = function(setup_completed,method_prompt){ | |||||
$('.login-content').empty().append($('<div>').attr({'id':'otp_div'}).html('<form class="form-verify"><div class="page-card-head">\ | |||||
<span class="indicator blue" data-text="Verification">Verification</span></div>\ | <span class="indicator blue" data-text="Verification">Verification</span></div>\ | ||||
<input type="text" id="login_token" class="form-control" placeholder="Verification Code" required="" autofocus="">\ | |||||
<input type="text" id="login_token" class="form-control" placeholder="Verification Code" required="" autocomplete="off" autofocus="">\ | |||||
<button class="btn btn-sm btn-primary btn-block" id="verify_token">Verify</button></form>')); | <button class="btn btn-sm btn-primary btn-block" id="verify_token">Verify</button></form>')); | ||||
if (!data.verification.setup_completed){ | |||||
var qrcode = $('<div>').attr('id','qrcode_div'); | |||||
var direction = $('<div>').attr('id','qr_info').text('Scan QR Code and enter the resulting code displayed'); | |||||
var qrcanvas = $('<canvas>'); | |||||
qrcanvas.attr('id','qrcanvass'); | |||||
qrcode.append(direction); | |||||
qrcode.append(qrcanvas); | |||||
$('#otp_div').prepend(qrcode) | |||||
qr = new QRious({ | |||||
element: document.getElementById('qrcanvass'), | |||||
value: data.verification.totp_uri, | |||||
background: 'white', // background color | |||||
foreground: 'black', // foreground color | |||||
level: 'L', // Error correction level of the QR code | |||||
mime: 'image/png', // MIME type used to render | |||||
size: 200 | |||||
verify_token(); | |||||
if (!setup_completed){ | |||||
var qrcode = $('<div>').attr('id','qrcode_div'); | |||||
var direction = $('<div>').attr('id','qr_info').text(method_prompt || 'Scan QR Code and enter the resulting code displayed'); | |||||
var qrcanvas = $('<canvas>'); | |||||
qrcanvas.attr('id','qrcanvass'); | |||||
qrcode.append(direction); | |||||
qrcode.append(qrcanvas); | |||||
$('#otp_div').prepend(qrcode) | |||||
qr = new QRious({ | |||||
element: document.getElementById('qrcanvass'), | |||||
value: data.verification.totp_uri, | |||||
background: 'white', // background color | |||||
foreground: 'black', // foreground color | |||||
level: 'L', // Error correction level of the QR code | |||||
mime: 'image/png', // MIME type used to render | |||||
size: 200 | |||||
}); | |||||
} else { | |||||
var qrcode = $('<div>').attr('id','qrcode_div'); | |||||
var direction = $('<div>').attr('id','qr_info').text(method_prompt || 'Enter Code displayed in OTP App'); | |||||
direction.attr('style','padding-bottom:10px;'); | |||||
qrcode.append(direction); | |||||
$('#otp_div').prepend(qrcode) | |||||
} | |||||
} | |||||
var continue_sms = function(setup_completed,method_prompt){ | |||||
$('.login-content').empty().append($('<div>').attr({'id':'otp_div'}).html( | |||||
'<form class="form-verify">\ | |||||
<div class="page-card-head">\ | |||||
<span class="indicator blue" data-text="Verification">Verification</span>\ | |||||
</div>\ | |||||
<input type="text" id="login_token" class="form-control" placeholder="Verification Code" required="" autofocus="">\ | |||||
<button class="btn btn-sm btn-primary btn-block" id="verify_token">Verify</button>\ | |||||
</form>')); | |||||
verify_token(); | |||||
if (!setup_completed){ | |||||
var sms_div = $('<div>').attr({'id':'sms_div','style':'margin-bottom: 20px;'}); | |||||
var direction = $('<div>').attr({'id':'sms_info','style':'margin-bottom: 15px;'}).text('Enter phone number to send verification code'); | |||||
sms_div.append(direction); | |||||
sms_div.append($('<div>').attr({'id':'sms_code_div'}).html( | |||||
'<div class="form-group text-center">\ | |||||
<input type="text" id="phone_no" class="form-control" placeholder="2347001234567" required="" autofocus="">\ | |||||
<button class="btn btn-sm btn-primary" id="submit_phone_no" >Send SMS</button>\ | |||||
</div><hr>')); | |||||
$('#otp_div').prepend(sms_div); | |||||
$('#submit_phone_no').on('click',function(){ | |||||
frappe.call({ | |||||
method: "frappe.core.doctype.user.user.send_token_via_sms", | |||||
args: {'phone_no': $('#phone_no').val(), 'tmp_id':data.tmp_id }, | |||||
freeze: true, | |||||
callback: function(r) { | |||||
if (r.message){ | |||||
$('#sms_div').empty().append( | |||||
'<p class="lead">SMS sent.<br><small><small>Enter verification code received</small></small></p><hr>' | |||||
); | |||||
} else { | |||||
$('#sms_div').empty().append( | |||||
'<p class="lead">SMS not sent</p><hr>' | |||||
); | |||||
} | |||||
} | |||||
}); | }); | ||||
}) | |||||
} else { | |||||
var smscode = $('<div>').attr('id','smscode_div'); | |||||
var direction = $('<div>').attr('id','qr_info').text(method_prompt || 'Enter verification code sent to registered phone number'); | |||||
direction.attr('style','padding-bottom:10px;'); | |||||
smscode.append(direction); | |||||
$('#otp_div').prepend(smscode) | |||||
} | |||||
} | |||||
var continue_email = function(setup_completed,method_prompt){ | |||||
$('.login-content').empty().append($('<div>').attr({'id':'otp_div'}).html( | |||||
'<form class="form-verify">\ | |||||
<div class="page-card-head">\ | |||||
<span class="indicator blue" data-text="Verification">Verification</span>\ | |||||
</div>\ | |||||
<input type="text" id="login_token" class="form-control" placeholder="Verification Code" required="" autofocus="">\ | |||||
<button class="btn btn-sm btn-primary btn-block" id="verify_token">Verify</button>\ | |||||
</form>')); | |||||
verify_token(); | |||||
if (!setup_completed){ | |||||
var email_div = $('<div>').attr({'id':'email_div','style':'margin-bottom: 20px;'}); | |||||
email_div.append('<p>Verification code email will be sent to registered email address. Enter code received below</p>') | |||||
$('#otp_div').prepend(email_div); | |||||
frappe.call({ | |||||
method: "frappe.core.doctype.user.user.send_token_via_email", | |||||
args: {'tmp_id':data.tmp_id }, | |||||
callback: function(r) { | |||||
if (r.message){ | |||||
} else { | |||||
$('#email_div').empty().append( | |||||
'<p>Email not sent</p><hr>' | |||||
); | |||||
} | |||||
} | |||||
}); | |||||
} else { | |||||
if (method_prompt){ | |||||
var emailcode = $('<div>').attr('id','emailcode_div'); | |||||
var direction = $('<div>').attr('id','qr_info').text(method_prompt || 'Verification code email will be sent to registered email address. Enter code received below'); | |||||
direction.attr('style','padding-bottom:10px;'); | |||||
emailcode.append(direction); | |||||
$('#otp_div').prepend(emailcode); | |||||
} else { | |||||
var emailcode = $('<div>').attr('id','emailcode_div'); | |||||
var direction = $('<div>').attr('id','qr_info').text('Verification code email not sent'); | |||||
direction.attr('style','padding-bottom:10px;'); | |||||
emailcode.append(direction); | |||||
$('#otp_div').prepend(emailcode) | |||||
} | |||||
} | |||||
} | |||||
if (data.verification.method_first_time){ | |||||
$('.login-content').empty().append('<div id="verification_method">\ | |||||
<div>\ | |||||
<p class="lead">Select verification Method <br>\ | |||||
<small><small><small class="text-muted">method may be changed later in settings</small></small></small></p>\ | |||||
</div>\ | |||||
<div class="form-check">\ | |||||
<label class="form-check-label">\ | |||||
<input class="form-check-input" type="radio" name="method" value="OTP App" checked>\ | |||||
OTP App\ | |||||
</label>\ | |||||
</div>\ | |||||
<div class="form-check">\ | |||||
<label class="form-check-label">\ | |||||
<input class="form-check-input" type="radio" name="method" value="SMS">\ | |||||
SMS\ | |||||
</label>\ | |||||
</div>\ | |||||
<div class="form-check disabled">\ | |||||
<label class="form-check-label">\ | |||||
<input class="form-check-input" type="radio" name="method" value="Email">\ | |||||
Email\ | |||||
</label>\ | |||||
</div>\ | |||||
<button id="submit_method" class="btn btn-sm btn-primary">Continue</button>\ | |||||
</div>') | |||||
if (data.verification.restrict_method){ | |||||
$('input[name=method]').each(function(){ | |||||
if ($(this).val() != data.verification.restrict_method){ | |||||
$(this).attr('disabled',true) | |||||
} | |||||
}) | |||||
} | |||||
$('#submit_method').on('click',function(event){ | |||||
if ($('input[name=method]:checked').val() == 'OTP App'){ | |||||
continue_otp(setup_completed=false); | |||||
} else if ($('input[name=method]:checked').val() == 'SMS'){ | |||||
continue_sms(setup_completed=false); | |||||
console.log('SMS'); | |||||
} else if ($('input[name=method]:checked').val() == 'Email'){ | |||||
continue_email(setup_completed=false); | |||||
} | |||||
frappe.call({ | |||||
method: "frappe.core.doctype.user.user.set_verification_method", | |||||
args: {'tmp_id':data.tmp_id, 'method': $('input[name=method]:checked').val()}, | |||||
callback: function(r) { } | |||||
}); | |||||
}); | |||||
} else { | } else { | ||||
var qrcode = $('<div>').attr('id','qrcode_div'); | |||||
var direction = $('<div>').attr('id','qr_info').text('Enter the code displayed in otp app under the appropriate account'); | |||||
direction.attr('style','padding-bottom:10px;'); | |||||
qrcode.append(direction); | |||||
$('#otp_div').prepend(qrcode) | |||||
if (data.verification.method == 'OTP App'){ | |||||
console.log(data.verification.totp_uri) | |||||
continue_otp(setup_completed = !data.verification.totp_uri); | |||||
} else if (data.verification.method == 'SMS'){ | |||||
continue_sms(setup_completed=true, method_prompt=data.verification.prompt); | |||||
console.log('SMS'); | |||||
} else if (data.verification.method == 'Email'){ | |||||
continue_sms(setup_completed=true, method_prompt=data.verification.prompt); | |||||
} | |||||
} | } | ||||
document.cookie = "tmp_id="+data.tmp_id; | document.cookie = "tmp_id="+data.tmp_id; | ||||
verify_token(); | |||||
//verify_token(); | |||||
return false; | return false; | ||||
} else if(data.message == 'Logged In'){ | } else if(data.message == 'Logged In'){ | ||||
login.set_indicator("{{ _("Success") }}", 'green'); | login.set_indicator("{{ _("Success") }}", 'green'); | ||||
window.location.href = get_url_arg("redirect-to") || data.home_page; | window.location.href = get_url_arg("redirect-to") || data.home_page; | ||||
@@ -12,6 +12,20 @@ from frappe.integrations.doctype.ldap_settings.ldap_settings import get_ldap_set | |||||
no_cache = True | no_cache = True | ||||
import pyqrcode | |||||
from StringIO import StringIO | |||||
from werkzeug.wrappers import Response | |||||
def get_qr_code(): | |||||
url = pyqrcode.create('http://www.google.com') | |||||
stream = StringIO() | |||||
url.svg(stream, scale=5) | |||||
responses = Response(stream.getvalue().encode('utf-8')) | |||||
responses.status_code = 200 | |||||
responses.headers['content-type'] = 'image/svg+xml; charset=utf-8' | |||||
return responses | |||||
def get_context(context): | def get_context(context): | ||||
if frappe.session.user != "Guest" and frappe.session.data.user_type=="System User": | if frappe.session.user != "Guest" and frappe.session.data.user_type=="System User": | ||||
frappe.local.flags.redirect_location = "/testpayment" | frappe.local.flags.redirect_location = "/testpayment" | ||||
@@ -30,6 +44,7 @@ def get_context(context): | |||||
ldap_settings = get_ldap_settings() | ldap_settings = get_ldap_settings() | ||||
context["ldap_settings"] = ldap_settings | context["ldap_settings"] = ldap_settings | ||||
context['qqrcode'] = frappe.render_template(get_qr_code()) | |||||
return context | return context | ||||