Browse Source

[WIP]Add QRCode email feature

version-14
B H Boma 8 years ago
parent
commit
94cc69dfa5
4 changed files with 253 additions and 18 deletions
  1. +128
    -4
      frappe/core/doctype/system_settings/system_settings.json
  2. +70
    -14
      frappe/twofactor.py
  3. +12
    -0
      frappe/www/qrcode.html
  4. +43
    -0
      frappe/www/qrcode.py

+ 128
- 4
frappe/core/doctype/system_settings/system_settings.json View File

@@ -751,7 +751,7 @@
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"OTP App\" && doc.send_barcode_as_email==1", "depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"OTP App\" && doc.send_barcode_as_email==1",
"description": "Time in seconds to retain barcode image on server. Min:<strong>240</strong>",
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
"fieldname": "lifespan_barcode_image", "fieldname": "lifespan_barcode_image",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0, "hidden": 0,
@@ -761,7 +761,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": "Delete Barcode Image On server",
"label": "Delete QR Code Image On server",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -1010,7 +1010,131 @@
"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": "Send Barcode as Email",
"label": "Send QR Code as email",
"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,
"depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"OTP App\" && doc.send_barcode_as_email==1",
"fieldname": "qr_code_email_subject",
"fieldtype": "Data",
"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": "QR Code Email Subject",
"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,
"depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"OTP App\" && doc.send_barcode_as_email==1",
"fieldname": "qr_code_email_body",
"fieldtype": "Small Text",
"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": "QR Code Email Body",
"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,
"depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"Email\"",
"fieldname": "two_factor_email_subject",
"fieldtype": "Data",
"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": "Two factor Email Subject",
"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,
"depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"Email\"",
"fieldname": "two_factor_email_body",
"fieldtype": "Small Text",
"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": "Two factor Email Body",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -1157,7 +1281,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-07-27 12:23:01.135841",
"modified": "2017-07-28 07:21:12.520227",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "System Settings", "name": "System Settings",


+ 70
- 14
frappe/twofactor.py View File

@@ -7,19 +7,20 @@ import frappe
from frappe import _ from frappe import _
import pyotp,base64,os import pyotp,base64,os
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from jinja2 import Template
from pyqrcode import create as qrcreate from pyqrcode import create as qrcreate
from StringIO import StringIO from StringIO import StringIO
from base64 import b64encode,b32encode from base64 import b64encode,b32encode
from frappe.utils import get_url, get_datetime, time_diff_in_seconds from frappe.utils import get_url, get_datetime, time_diff_in_seconds




class ExpiredLoginExpection(Exception):pass
class ExpiredLoginException(Exception):pass


def should_run_2fa(user): def should_run_2fa(user):
'''Check if 2fa should run.''' '''Check if 2fa should run.'''
site_otp_enabled = frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') site_otp_enabled = frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth')
user_otp_enabled = two_factor_is_enabled_for_(user) user_otp_enabled = two_factor_is_enabled_for_(user)
#Don't validate for Admin of if not enabled
#Don't validate for Admin or if not enabled
if user =='Administrator' or not site_otp_enabled or not user_otp_enabled: if user =='Administrator' or not site_otp_enabled or not user_otp_enabled:
return False return False
return True return True
@@ -102,7 +103,7 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None):
hotp_token = frappe.cache().get(tmp_id + '_token') hotp_token = frappe.cache().get(tmp_id + '_token')
otp_secret = frappe.cache().get(tmp_id + '_otp_secret') otp_secret = frappe.cache().get(tmp_id + '_otp_secret')
if not otp_secret: if not otp_secret:
raise ExpiredLoginExpection(_('Login session expired, refresh page to retry'))
raise ExpiredLoginException(_('Login session expired, refresh page to retry'))
hotp = pyotp.HOTP(otp_secret) hotp = pyotp.HOTP(otp_secret)
if hotp_token: if hotp_token:
if hotp.verify(otp, int(hotp_token)): if hotp.verify(otp, int(hotp_token)):
@@ -119,7 +120,7 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None):
delete_qrimage(login_manager.user) delete_qrimage(login_manager.user)
return True return True
else: else:
login_manager.fail('Incorrect Verification code', login_manager.user)
login_manager.fail(_('Incorrect Verification code'), login_manager.user)




def get_verification_obj(user,token,otp_secret): def get_verification_obj(user,token,otp_secret):
@@ -166,20 +167,71 @@ def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='email'):
'''Process Email method for 2fa.''' '''Process Email method for 2fa.'''
message = None message = None
status = True status = True
# TODO SVG don't display in email
if method == 'otp_app' and not frappe.db.get_default(user + '_otplogin'): if method == 'otp_app' and not frappe.db.get_default(user + '_otplogin'):
totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
message = '''<p>Please scan the barcode for One Time Password</p>
<img src="{}"
style':'width:150px;height:150px;>'''.format(qrcode_as_png(user,totp_uri))
qrcode_link = get_link_for_qrcode(user,totp_uri)
message = get_email_body_for_qr_code({'qrcode_link':qrcode_link})
subject = get_email_subject_for_qr_code({'qrcode_link':qrcode_link})
if method == 'email' or message: if method == 'email' or message:
status = send_token_via_email(user,token,otp_secret,otp_issuer,message=message)
status = send_token_via_email(user,token,otp_secret,otp_issuer,subject=subject,message=message)
verification_obj = {'token_delivery': status, verification_obj = {'token_delivery': status,
'prompt': status and 'Verification code has been sent to your registered email address', 'prompt': status and 'Verification code has been sent to your registered email address',
'method': 'Email'} 'method': 'Email'}
return verification_obj return verification_obj



def get_email_subject_for_2fa(kwargs_dict):
'''Get email subject for 2fa.'''
subject_template = 'Verifcation Code from Frappe Framework'
template = frappe.get_value('System Settings','System Settings','two_factor_email_subject')
if not template == '':
subject_template = template
subject = render_string_template(subject_template,kwargs_dict)
return subject

def get_email_body_for_2fa(kwargs_dict):
'''Get email body for 2fa.'''
body_template = 'Use this token to login <br> {{otp}}'
template = frappe.get_value('System Settings','System Settings','two_factor_email_body')
if not template == '':
subject_template = template
body = render_string_template(body_template,kwargs_dict)
return body

def get_email_subject_for_qr_code(kwargs_dict):
'''Get QRCode email subject.'''
subject_template = 'Verification Code from Frappe Framework'
template = frappe.get_value('System Settings','System Settings','qr_code_email_subject')
if not template == '':
subject_template = template
subject = render_string_template(subject_template,kwargs_dict)
return subject

def get_email_body_for_qr_code(kwargs_dict):
'''Get QRCode email body.'''
body_template = 'Scan the QRCode on this link to get token <br> {{qrcode_link}}'
template = frappe.get_value('System Settings','System Settings','qr_code_email_body')
if not template == '':
body_template = template
body = render_string_template(body_template,kwargs_dict)
return body

def render_string_template(_str,kwargs_dict):
'''Render string with jinja.'''
s = Template(_str)
s = s.render(**kwargs_dict)
return s

def get_link_for_qrcode(user,totp_uri):
'''Get link to temporary page showing QRCode.'''
key = frappe.generate_hash(length=20)
key_user = "{}_user".format(key)
key_uri = "{}_uri".format(key)
lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_barcode_image'))
if lifespan<=0:
lifespan = 240
frappe.cache().set_value(key_uri,totp_uri,expires_in_sec=lifespan)
frappe.cache().set_value(key_user,user,expires_in_sec=lifespan)
return get_url('/qrcode?k={}'.format(key))


def send_token_via_sms(otpsecret, token=None, phone_no=None): def send_token_via_sms(otpsecret, token=None, phone_no=None):
'''Send token as sms to user.''' '''Send token as sms to user.'''
@@ -207,16 +259,20 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None):
enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args) enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args)
return True return True


def send_token_via_email(user, token, otp_secret, otp_issuer,message=None):
def send_token_via_email(user, token, otp_secret, otp_issuer,subject=None,message=None):
'''Send token to user as email.''' '''Send token to user as email.'''
user_email = frappe.db.get_value('User', user, 'email') user_email = frappe.db.get_value('User', user, 'email')
if not user_email: if not user_email:
return False return False
hotp = pyotp.HOTP(otp_secret) hotp = pyotp.HOTP(otp_secret)
otp = hotp.at(int(token))
template_args = {'otp':otp,'otp_issuer':otp_issuer}
if not subject:
subject = get_email_subject_for_2fa(template_args)
if not message: if not message:
message = '<p>Your verification code is {}.</p>'.format(hotp.at(int(token)))
message = get_email_body_for_2fa(template_args)
email_args = { email_args = {
'recipients':user_email, 'sender':None, 'subject':'Verification Code from {}'.format(otp_issuer or "Frappe Framework"),
'recipients':user_email, 'sender':None, 'subject':subject,
'message':message, 'message':message,
'delayed':False, 'retry':3 } 'delayed':False, 'retry':3 }


@@ -236,7 +292,7 @@ def get_qr_svg_code(totp_uri):
svg = '' svg = ''
stream = StringIO() stream = StringIO()
try: try:
url.svg(stream, scale=3)
url.svg(stream, scale=4, background="#eee", module_color="#222")
svg = stream.getvalue().replace('\n','') svg = stream.getvalue().replace('\n','')
svg = b64encode(bytes(svg)) svg = b64encode(bytes(svg))
finally: finally:


+ 12
- 0
frappe/www/qrcode.html View File

@@ -0,0 +1,12 @@
<div>
<div style="text-align:center">
<div style="width:400px;margin:auto;text-align:center;">
<strong style="padding:10px;">Hi {{qr_code_user.first_name}}, Please scan QR Code and enter the resulting code displayed.
You can use apps such as Google Authenticator, Lastpass Authenticator, Authy, Duo Mobile and others.
</strong>
</div>
<div style="margin-top:10px;">
<img src="data:image/svg+xml;base64,{{qrcode_svg}}">
</div>
</div>
</div>

+ 43
- 0
frappe/www/qrcode.py View File

@@ -0,0 +1,43 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals

import frappe
from frappe import _
from urlparse import parse_qs
from frappe.twofactor import get_qr_svg_code

no_cache = 1


def get_context(context):
context.qr_code_user,context.qrcode_svg = get_user_svg_from_cache()





def get_query_key():
'''Return query string arg.'''
query_string = frappe.local.request.query_string
query = parse_qs(query_string)
if not 'k' in query.keys():
frappe.throw(_('Not Permitted'),frappe.PermissionError)
query = (query['k'][0]).strip()
if False in [i.isalpha() or i.isdigit() for i in query]:
frappe.throw(_('Not Permitted'),frappe.PermissionError)
return query

def get_user_svg_from_cache():
'''Get User and SVG code from cache.'''
key = get_query_key()
totp_uri = frappe.cache().get_value("{}_uri".format(key))
user = frappe.cache().get_value("{}_user".format(key))
if not totp_uri or not user:
frappe.throw(_('Page has expired!'),frappe.PermissionError)
if not frappe.db.exists('User',user):
frappe.throw(_('Not Permitted'), frappe.PermissionError)
user = frappe.get_doc('User',user)
svg = get_qr_svg_code(totp_uri)
return (user,svg)

Loading…
Cancel
Save