You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

419 lines
14 KiB

  1. # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. import frappe
  4. from frappe import _
  5. import pyotp, os
  6. from frappe.utils.background_jobs import enqueue
  7. from pyqrcode import create as qrcreate
  8. from io import BytesIO
  9. from base64 import b64encode, b32encode
  10. from frappe.utils import get_url, get_datetime, time_diff_in_seconds, cint
  11. class ExpiredLoginException(Exception): pass
  12. def toggle_two_factor_auth(state, roles=None):
  13. '''Enable or disable 2FA in site_config and roles'''
  14. for role in roles or []:
  15. role = frappe.get_doc('Role', {'role_name': role})
  16. role.two_factor_auth = cint(state)
  17. role.save(ignore_permissions=True)
  18. def two_factor_is_enabled(user=None):
  19. '''Returns True if 2FA is enabled.'''
  20. enabled = int(frappe.db.get_value('System Settings', None, 'enable_two_factor_auth') or 0)
  21. if enabled:
  22. bypass_two_factor_auth = int(frappe.db.get_value('System Settings', None, 'bypass_2fa_for_retricted_ip_users') or 0)
  23. if bypass_two_factor_auth and user:
  24. user_doc = frappe.get_doc("User", user)
  25. restrict_ip_list = user_doc.get_restricted_ip_list() #can be None or one or more than one ip address
  26. if restrict_ip_list and frappe.local.request_ip:
  27. for ip in restrict_ip_list:
  28. if frappe.local.request_ip.startswith(ip):
  29. enabled = False
  30. break
  31. if not user or not enabled:
  32. return enabled
  33. return two_factor_is_enabled_for_(user)
  34. def should_run_2fa(user):
  35. '''Check if 2fa should run.'''
  36. return two_factor_is_enabled(user=user)
  37. def get_cached_user_pass():
  38. '''Get user and password if set.'''
  39. user = pwd = None
  40. tmp_id = frappe.form_dict.get('tmp_id')
  41. if tmp_id:
  42. user = frappe.safe_decode(frappe.cache().get(tmp_id+'_usr'))
  43. pwd = frappe.safe_decode(frappe.cache().get(tmp_id+'_pwd'))
  44. return (user, pwd)
  45. def authenticate_for_2factor(user):
  46. '''Authenticate two factor for enabled user before login.'''
  47. if frappe.form_dict.get('otp'):
  48. return
  49. otp_secret = get_otpsecret_for_(user)
  50. token = int(pyotp.TOTP(otp_secret).now())
  51. tmp_id = frappe.generate_hash(length=8)
  52. cache_2fa_data(user, token, otp_secret, tmp_id)
  53. verification_obj = get_verification_obj(user, token, otp_secret)
  54. # Save data in local
  55. frappe.local.response['verification'] = verification_obj
  56. frappe.local.response['tmp_id'] = tmp_id
  57. def cache_2fa_data(user, token, otp_secret, tmp_id):
  58. '''Cache and set expiry for data.'''
  59. pwd = frappe.form_dict.get('pwd')
  60. verification_method = get_verification_method()
  61. # set increased expiry time for SMS and Email
  62. if verification_method in ['SMS', 'Email']:
  63. expiry_time = frappe.flags.token_expiry or 300
  64. frappe.cache().set(tmp_id + '_token', token)
  65. frappe.cache().expire(tmp_id + '_token', expiry_time)
  66. else:
  67. expiry_time = frappe.flags.otp_expiry or 180
  68. for k, v in {'_usr': user, '_pwd': pwd, '_otp_secret': otp_secret}.items():
  69. frappe.cache().set("{0}{1}".format(tmp_id, k), v)
  70. frappe.cache().expire("{0}{1}".format(tmp_id, k), expiry_time)
  71. def two_factor_is_enabled_for_(user):
  72. '''Check if 2factor is enabled for user.'''
  73. if user == "Administrator":
  74. return False
  75. if isinstance(user, str):
  76. user = frappe.get_doc("User", user)
  77. roles = [d.role for d in user.roles or []] + ["All"]
  78. role_doctype = frappe.qb.DocType("Role")
  79. no_of_users = frappe.db.count(role_doctype, filters=
  80. ((role_doctype.two_factor_auth == 1) & (role_doctype.name.isin(roles))),
  81. )
  82. if int(no_of_users) > 0:
  83. return True
  84. return False
  85. def get_otpsecret_for_(user):
  86. '''Set OTP Secret for user even if not set.'''
  87. otp_secret = frappe.db.get_default(user + '_otpsecret')
  88. if not otp_secret:
  89. otp_secret = b32encode(os.urandom(10)).decode('utf-8')
  90. frappe.db.set_default(user + '_otpsecret', otp_secret)
  91. frappe.db.commit()
  92. return otp_secret
  93. def get_verification_method():
  94. return frappe.db.get_value('System Settings', None, 'two_factor_method')
  95. def confirm_otp_token(login_manager, otp=None, tmp_id=None):
  96. '''Confirm otp matches.'''
  97. from frappe.auth import get_login_attempt_tracker
  98. if not otp:
  99. otp = frappe.form_dict.get('otp')
  100. if not otp:
  101. if two_factor_is_enabled_for_(login_manager.user):
  102. return False
  103. return True
  104. if not tmp_id:
  105. tmp_id = frappe.form_dict.get('tmp_id')
  106. hotp_token = frappe.cache().get(tmp_id + '_token')
  107. otp_secret = frappe.cache().get(tmp_id + '_otp_secret')
  108. if not otp_secret:
  109. raise ExpiredLoginException(_('Login session expired, refresh page to retry'))
  110. tracker = get_login_attempt_tracker(login_manager.user)
  111. hotp = pyotp.HOTP(otp_secret)
  112. if hotp_token:
  113. if hotp.verify(otp, int(hotp_token)):
  114. frappe.cache().delete(tmp_id + '_token')
  115. tracker.add_success_attempt()
  116. return True
  117. else:
  118. tracker.add_failure_attempt()
  119. login_manager.fail(_('Incorrect Verification code'), login_manager.user)
  120. totp = pyotp.TOTP(otp_secret)
  121. if totp.verify(otp):
  122. # show qr code only once
  123. if not frappe.db.get_default(login_manager.user + '_otplogin'):
  124. frappe.db.set_default(login_manager.user + '_otplogin', 1)
  125. delete_qrimage(login_manager.user)
  126. tracker.add_success_attempt()
  127. return True
  128. else:
  129. tracker.add_failure_attempt()
  130. login_manager.fail(_('Incorrect Verification code'), login_manager.user)
  131. def get_verification_obj(user, token, otp_secret):
  132. otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
  133. verification_method = get_verification_method()
  134. verification_obj = None
  135. if verification_method == 'SMS':
  136. verification_obj = process_2fa_for_sms(user, token, otp_secret)
  137. elif verification_method == 'OTP App':
  138. #check if this if the first time that the user is trying to login. If so, send an email
  139. if not frappe.db.get_default(user + '_otplogin'):
  140. verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer, method='OTP App')
  141. else:
  142. verification_obj = process_2fa_for_otp_app(user, otp_secret, otp_issuer)
  143. elif verification_method == 'Email':
  144. verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer)
  145. return verification_obj
  146. def process_2fa_for_sms(user, token, otp_secret):
  147. '''Process sms method for 2fa.'''
  148. phone = frappe.db.get_value('User', user, ['phone', 'mobile_no'], as_dict=1)
  149. phone = phone.mobile_no or phone.phone
  150. status = send_token_via_sms(otp_secret, token=token, phone_no=phone)
  151. verification_obj = {
  152. 'token_delivery': status,
  153. 'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]),
  154. 'method': 'SMS',
  155. 'setup': status
  156. }
  157. return verification_obj
  158. def process_2fa_for_otp_app(user, otp_secret, otp_issuer):
  159. '''Process OTP App method for 2fa.'''
  160. totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
  161. if frappe.db.get_default(user + '_otplogin'):
  162. otp_setup_completed = True
  163. else:
  164. otp_setup_completed = False
  165. verification_obj = {
  166. 'method': 'OTP App',
  167. 'setup': otp_setup_completed
  168. }
  169. return verification_obj
  170. def process_2fa_for_email(user, token, otp_secret, otp_issuer, method='Email'):
  171. '''Process Email method for 2fa.'''
  172. subject = None
  173. message = None
  174. status = True
  175. prompt = ''
  176. if method == 'OTP App' and not frappe.db.get_default(user + '_otplogin'):
  177. '''Sending one-time email for OTP App'''
  178. totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
  179. qrcode_link = get_link_for_qrcode(user, totp_uri)
  180. message = get_email_body_for_qr_code({'qrcode_link': qrcode_link})
  181. subject = get_email_subject_for_qr_code({'qrcode_link': qrcode_link})
  182. prompt = _('Please check your registered email address for instructions on how to proceed. Do not close this window as you will have to return to it.')
  183. else:
  184. '''Sending email verification'''
  185. prompt = _('Verification code has been sent to your registered email address.')
  186. status = send_token_via_email(user, token, otp_secret, otp_issuer, subject=subject, message=message)
  187. verification_obj = {
  188. 'token_delivery': status,
  189. 'prompt': status and prompt,
  190. 'method': 'Email',
  191. 'setup': status
  192. }
  193. return verification_obj
  194. def get_email_subject_for_2fa(kwargs_dict):
  195. '''Get email subject for 2fa.'''
  196. subject_template = _('Login Verification Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
  197. subject = frappe.render_template(subject_template, kwargs_dict)
  198. return subject
  199. def get_email_body_for_2fa(kwargs_dict):
  200. '''Get email body for 2fa.'''
  201. body_template = """
  202. Enter this code to complete your login:
  203. <br><br>
  204. <b style="font-size: 18px;">{{ otp }}</b>
  205. """
  206. body = frappe.render_template(body_template, kwargs_dict)
  207. return body
  208. def get_email_subject_for_qr_code(kwargs_dict):
  209. '''Get QRCode email subject.'''
  210. subject_template = _('One Time Password (OTP) Registration Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
  211. subject = frappe.render_template(subject_template, kwargs_dict)
  212. return subject
  213. def get_email_body_for_qr_code(kwargs_dict):
  214. '''Get QRCode email body.'''
  215. body_template = 'Please click on the following link and follow the instructions on the page.<br><br> {{qrcode_link}}'
  216. body = frappe.render_template(body_template, kwargs_dict)
  217. return body
  218. def get_link_for_qrcode(user, totp_uri):
  219. '''Get link to temporary page showing QRCode.'''
  220. key = frappe.generate_hash(length=20)
  221. key_user = "{}_user".format(key)
  222. key_uri = "{}_uri".format(key)
  223. lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')) or 240
  224. frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan)
  225. frappe.cache().set_value(key_user, user, expires_in_sec=lifespan)
  226. return get_url('/qrcode?k={}'.format(key))
  227. def send_token_via_sms(otpsecret, token=None, phone_no=None):
  228. '''Send token as sms to user.'''
  229. try:
  230. from frappe.core.doctype.sms_settings.sms_settings import send_request
  231. except:
  232. return False
  233. if not phone_no:
  234. return False
  235. ss = frappe.get_doc('SMS Settings', 'SMS Settings')
  236. if not ss.sms_gateway_url:
  237. return False
  238. hotp = pyotp.HOTP(otpsecret)
  239. args = {
  240. ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token)))
  241. }
  242. for d in ss.get("parameters"):
  243. args[d.parameter] = d.value
  244. args[ss.receiver_parameter] = phone_no
  245. sms_args = {
  246. 'params': args,
  247. 'gateway_url': ss.sms_gateway_url,
  248. 'use_post': ss.use_post
  249. }
  250. enqueue(method=send_request, queue='short', timeout=300, event=None,
  251. is_async=True, job_name=None, now=False, **sms_args)
  252. return True
  253. def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, message=None):
  254. '''Send token to user as email.'''
  255. user_email = frappe.db.get_value('User', user, 'email')
  256. if not user_email:
  257. return False
  258. hotp = pyotp.HOTP(otp_secret)
  259. otp = hotp.at(int(token))
  260. template_args = {'otp': otp, 'otp_issuer': otp_issuer}
  261. if not subject:
  262. subject = get_email_subject_for_2fa(template_args)
  263. if not message:
  264. message = get_email_body_for_2fa(template_args)
  265. email_args = {
  266. 'recipients': user_email,
  267. 'sender': None,
  268. 'subject': subject,
  269. 'message': message,
  270. 'header': [_('Verfication Code'), 'blue'],
  271. 'delayed': False,
  272. 'retry':3
  273. }
  274. enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None,
  275. is_async=True, job_name=None, now=False, **email_args)
  276. return True
  277. def get_qr_svg_code(totp_uri):
  278. '''Get SVG code to display Qrcode for OTP.'''
  279. url = qrcreate(totp_uri)
  280. svg = ''
  281. stream = BytesIO()
  282. try:
  283. url.svg(stream, scale=4, background="#eee", module_color="#222")
  284. svg = stream.getvalue().decode().replace('\n', '')
  285. svg = b64encode(svg.encode())
  286. finally:
  287. stream.close()
  288. return svg
  289. def qrcode_as_png(user, totp_uri):
  290. '''Save temporary Qrcode to server.'''
  291. folder = create_barcode_folder()
  292. png_file_name = '{}.png'.format(frappe.generate_hash(length=20))
  293. _file = frappe.get_doc({
  294. "doctype": "File",
  295. "file_name": png_file_name,
  296. "attached_to_doctype": 'User',
  297. "attached_to_name": user,
  298. "folder": folder,
  299. "content": png_file_name})
  300. _file.save()
  301. frappe.db.commit()
  302. file_url = get_url(_file.file_url)
  303. file_path = os.path.join(frappe.get_site_path('public', 'files'), _file.file_name)
  304. url = qrcreate(totp_uri)
  305. with open(file_path, 'w') as png_file:
  306. url.png(png_file, scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc])
  307. return file_url
  308. def create_barcode_folder():
  309. '''Get Barcodes folder.'''
  310. folder_name = 'Barcodes'
  311. folder = frappe.db.exists('File', {'file_name': folder_name})
  312. if folder:
  313. return folder
  314. folder = frappe.get_doc({
  315. 'doctype': 'File',
  316. 'file_name': folder_name,
  317. 'is_folder':1,
  318. 'folder': 'Home'
  319. })
  320. folder.insert(ignore_permissions=True)
  321. return folder.name
  322. def delete_qrimage(user, check_expiry=False):
  323. '''Delete Qrimage when user logs in.'''
  324. user_barcodes = frappe.get_all('File', {'attached_to_doctype': 'User',
  325. 'attached_to_name': user, 'folder': 'Home/Barcodes'})
  326. for barcode in user_barcodes:
  327. if check_expiry and not should_remove_barcode_image(barcode):
  328. continue
  329. barcode = frappe.get_doc('File', barcode.name)
  330. frappe.delete_doc('File', barcode.name, ignore_permissions=True)
  331. def delete_all_barcodes_for_users():
  332. '''Task to delete all barcodes for user.'''
  333. users = frappe.get_all('User', {'enabled':1})
  334. for user in users:
  335. if not two_factor_is_enabled(user=user.name):
  336. continue
  337. delete_qrimage(user.name, check_expiry=True)
  338. def should_remove_barcode_image(barcode):
  339. '''Check if it's time to delete barcode image from server. '''
  340. if isinstance(barcode, str):
  341. barcode = frappe.get_doc('File', barcode)
  342. lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') or 240
  343. if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan):
  344. return True
  345. return False
  346. def disable():
  347. frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 0)
  348. @frappe.whitelist()
  349. def reset_otp_secret(user):
  350. otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
  351. user_email = frappe.db.get_value('User', user, 'email')
  352. if frappe.session.user in ["Administrator", user] :
  353. frappe.defaults.clear_default(user + '_otplogin')
  354. frappe.defaults.clear_default(user + '_otpsecret')
  355. email_args = {
  356. 'recipients': user_email,
  357. 'sender': None,
  358. 'subject': _('OTP Secret Reset - {0}').format(otp_issuer or "Frappe Framework"),
  359. 'message': _('<p>Your OTP secret on {0} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.</p>').format(otp_issuer or "Frappe Framework"),
  360. 'delayed':False,
  361. 'retry':3
  362. }
  363. enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, is_async=True, job_name=None, now=False, **email_args)
  364. return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))
  365. else:
  366. return frappe.throw(_("OTP secret can only be reset by the Administrator."))