From 977130807234d75a32187673254d0ba1e007f033 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Wed, 28 Jun 2017 18:02:20 +0100 Subject: [PATCH 01/67] Check if user role on login, return otpauth uri --- frappe/auth.py | 70 ++++++++++++++++++++++-- frappe/core/doctype/role/role.json | 33 ++++++++++- frappe/core/doctype/user/user.json | 64 +++++++++++++++++++++- frappe/core/doctype/user/user.py | 9 +++ frappe/exceptions.py | 3 + frappe/templates/includes/login/login.js | 46 +++++++++++++++- frappe/www/login.py | 2 +- 7 files changed, 217 insertions(+), 10 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 3612b98f04..0a9a7ade5f 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -19,6 +19,8 @@ from frappe.core.doctype.authentication_log.authentication_log import add_authen from urllib import quote +import pyotp + class HTTPRequest: def __init__(self): # Get Environment variables @@ -116,15 +118,73 @@ class LoginManager: def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) - self.authenticate() - self.post_login() + otp = frappe.form_dict.get('otp') + if not otp: + self.authenticate() + # 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: + + if user_info.two_factor_setup: + frappe.local.response['verification'] = {'setup_completed':True} + raise frappe.RequestToken + otp_secret = frappe.db.get_default(self.user + '_otpsecret') + else: + import os + import base64 + otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') + frappe.db.set_default(self.user + '_otpsecret', otp_secret) + # set two_factor_setup as 1 meaning user has copied otpsecret + frappe.db.set_value("User", self.user, 'two_factor_setup', 1) + frappe.db.commit() + 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} + + tmp_id = frappe.generate_hash(length=8) + usr = frappe.form_dict.get('usr') + pwd = frappe.form_dict.get('pwd') + frappe.cache().hset('token',tmp_id,{'usr':usr,'pwd':pwd,'otp_secret':otp_secret}) + frappe.local.response['tmp_id'] = tmp_id + + raise frappe.RequestToken + + else: + self.post_login(no_two_auth=True) - def post_login(self): + else: + try: + tmp_info = frappe.cache().hget('token', frappe.form_dict.get('tmp_id')) + self.authenticate(user=tmp_info['usr'], pwd=tmp_info['pwd']) + except: + frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") + #frappe.respond_as_web_page("Logged Out", """

You have been logged out.

Back to Home

""") + #frappe.throw("+++++ YOUR LOGIN WAS SUCCESSFUL, CONGRATS +++++") + #frappe.website.render('/404.html') + self.post_login() + + def post_login(self,no_two_auth=False): self.run_trigger('on_login') self.validate_ip_address() self.validate_hour() - self.make_session() - self.set_user_info() + 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')) + self.make_session() + self.set_user_info() + else: + self.make_session() + self.set_user_info() + + def confirm_token(self,otp=None, tmp_id=None): + try: + otp_secret = frappe.cache().hget('token',tmp_id).get('otp_secret') + except AttributeError: + return False + totp = pyotp.TOTP(otp_secret) + if totp.verify(otp): + frappe.cache().hdel('token', tmp_id) + return True + else: + self.fail('Incorrect Verification code', user=frappe.cache().hget('token',tmp_id).get('usr')) def set_user_info(self, resume=False): # set sid again diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 104ee7d53c..a97def1910 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -105,6 +105,37 @@ "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_auth", + "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": "Two Factor Authenticaction", + "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, @@ -148,7 +179,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-05-04 11:03:41.533058", + "modified": "2017-06-28 13:29:49.915545", "modified_by": "Administrator", "module": "Core", "name": "Role", diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 3eda403272..809a9ea2e7 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1956,6 +1956,68 @@ "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_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, @@ -1971,7 +2033,7 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-05-19 09:12:35.697915", + "modified": "2017-06-28 14:40:26.616254", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index c91c876680..321b4f9304 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -57,6 +57,7 @@ class User(Document): self.validate_email_type(self.name) self.add_system_manager_role() self.set_system_user() + self.set_two_factor_auth() self.set_full_name() self.check_enable_disable() self.ensure_unique_roles() @@ -146,6 +147,14 @@ class User(Document): else: self.user_type = 'Website User' + def set_two_factor_auth(self): + '''Set two factor authentication for user''' + if (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]))): + self.two_factor_auth = 1 + def has_desk_access(self): '''Return true if any of the set roles has desk access''' if not self.roles: diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 723c602496..ae9fca7e7a 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -37,6 +37,9 @@ class SessionStopped(Exception): class UnsupportedMediaType(Exception): http_status_code = 415 +class RequestToken(Exception): + http_status_code = 200 + class Redirect(Exception): http_status_code = 301 diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 249487333e..afbfc8656f 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -5,11 +5,14 @@ window.disable_signup = {{ disable_signup and "true" or "false" }}; window.login = {}; +window.verify = {}; + login.bind_events = function() { $(window).on("hashchange", function() { login.route(); }); + $(".form-login").on("submit", function(event) { event.preventDefault(); var args = {}; @@ -90,6 +93,11 @@ login.login = function() { $(".for-login").toggle(true); } +login.steptwo = function() { + login.reset_sections(); + $(".for-login").toggle(true); +} + login.forgot = function() { login.reset_sections(); $(".for-forgot").toggle(true); @@ -148,7 +156,17 @@ login.login_handlers = (function() { var login_handlers = { 200: function(data) { - if(data.message=="Logged In") { + console.log(data); + if(data.token) { + login.set_indicator("{{ _("Success") }}", 'green'); + $('.login-content').empty().append($('
').html('
\ + Verification
\ + \ +
')); + document.cookie = "tmp_id="+data.tmp_id; + verify_token(); + return false; + } else if(data.message == 'Logged In'){ login.set_indicator("{{ _("Success") }}", 'green'); window.location.href = get_url_arg("redirect-to") || data.home_page; } else if(data.message=="No App") { @@ -194,10 +212,14 @@ login.login_handlers = (function() { }; return login_handlers; -})(); +} )(); frappe.ready(function() { + + login.bind_events(); + console.log("Why"); + if (!window.location.hash) { window.location.hash = "#login"; @@ -208,3 +230,23 @@ frappe.ready(function() { $(".form-signup, .form-forgot").removeClass("hide"); $(document).trigger('login_rendered'); }); + +var verify_token = function(event) { + $('#verify_token').bind("click", function() { + console.log("Why XX2"); + //eventx.preventDefault(); + var args = {}; + args.cmd = "login"; + args.otp = $("#login_token").val(); + console.log("LLLLLLLLLLLLLLLLLLL"); + args.tmp_id = frappe.get_cookie('tmp_id'); + if(!args.otp) { + frappe.msgprint('{{ _("Login token required") }}'); + return false; + } + console.log("Button Clicked") + console.log(args) + login.call(args); + return false; + }); +} diff --git a/frappe/www/login.py b/frappe/www/login.py index cc149abbec..dbfc6f6fe5 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -14,7 +14,7 @@ no_cache = True def get_context(context): if frappe.session.user != "Guest" and frappe.session.data.user_type=="System User": - frappe.local.flags.redirect_location = "/desk" + frappe.local.flags.redirect_location = "/testpayment" raise frappe.Redirect # get settings from site config From 9c144312eb6a88eaf81294af59a5160f094138b0 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Thu, 29 Jun 2017 16:34:01 +0100 Subject: [PATCH 02/67] show qr code for first otp login --- frappe/auth.py | 1 - frappe/hooks.py | 3 ++- frappe/public/build.json | 3 +++ frappe/public/js/frappe/qrious.min.js | 6 +++++ frappe/templates/includes/login/login.js | 28 ++++++++++++++++++++++-- 5 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 frappe/public/js/frappe/qrious.min.js diff --git a/frappe/auth.py b/frappe/auth.py index 0a9a7ade5f..94241a8e34 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -127,7 +127,6 @@ class LoginManager: if user_info.two_factor_setup: frappe.local.response['verification'] = {'setup_completed':True} - raise frappe.RequestToken otp_secret = frappe.db.get_default(self.user + '_otpsecret') else: import os diff --git a/frappe/hooks.py b/frappe/hooks.py index 2d28b74d91..1ceab9f7ca 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -40,7 +40,8 @@ app_include_css = [ ] web_include_js = [ - "website_script.js" + "website_script.js", + "assets/js/qrious.min.js" ] bootstrap = "assets/frappe/css/bootstrap.css" diff --git a/frappe/public/build.json b/frappe/public/build.json index 13bc48b8b5..546b90a82a 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -22,6 +22,9 @@ "website/js/website.js", "public/js/frappe/misc/rating_icons.html" ], + "js/qrious.min.js": [ + "public/js/frappe/qrious.min.js" + ], "js/dialog.min.js": [ "public/js/frappe/dom.js", "public/js/frappe/ui/modal.html", diff --git a/frappe/public/js/frappe/qrious.min.js b/frappe/public/js/frappe/qrious.min.js new file mode 100644 index 0000000000..5735ea62de --- /dev/null +++ b/frappe/public/js/frappe/qrious.min.js @@ -0,0 +1,6 @@ +/*! QRious v4.0.2 | (C) 2017 Alasdair Mercer | GPL v3 License +Based on jsqrencode | (C) 2010 tz@execpc.com | GPL v3 License +*/ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.QRious=e()}(this,function(){"use strict";function t(t,e){var n;return"function"==typeof Object.create?n=Object.create(t):(s.prototype=t,n=new s,s.prototype=null),e&&i(!0,n,e),n}function e(e,n,s,r){var o=this;return"string"!=typeof e&&(r=s,s=n,n=e,e=null),"function"!=typeof n&&(r=s,s=n,n=function(){return o.apply(this,arguments)}),i(!1,n,o,r),n.prototype=t(o.prototype,s),n.prototype.constructor=n,n.class_=e||o.class_,n.super_=o,n}function i(t,e,i){for(var n,s,a=0,h=(i=o.call(arguments,2)).length;a>1&1,n=0;n0;e--)n[e]=n[e]?n[e-1]^_.EXPONENT[v._modN(_.LOG[n[e]]+t)]:n[e-1];n[0]=_.EXPONENT[v._modN(_.LOG[n[0]]+t)]}for(t=0;t<=i;t++)n[t]=_.LOG[n[t]]},_checkBadness:function(){var t,e,i,n,s,r=0,o=this._badness,a=this.buffer,h=this.width;for(s=0;sh*h;)u-=h*h,c++;for(r+=c*v.N4,n=0;n=o-2&&(t=o-2,s>9&&t--);var a=t;if(s>9){for(r[a+2]=0,r[a+3]=0;a--;)e=r[a],r[a+3]|=255&e<<4,r[a+2]=e>>4;r[2]|=255&t<<4,r[1]=t>>4,r[0]=64|t>>12}else{for(r[a+1]=0,r[a+2]=0;a--;)e=r[a],r[a+2]|=255&e<<4,r[a+1]=e>>4;r[1]|=255&t<<4,r[0]=64|t>>4}for(a=t+3-(s<10);a=5&&(i+=v.N1+n[e]-5);for(e=3;et||3*n[e-3]>=4*n[e]||3*n[e+3]>=4*n[e])&&(i+=v.N3);return i},_finish:function(){this._stringBuffer=this.buffer.slice();var t,e,i=0,n=3e4;for(e=0;e<8&&(this._applyMask(e),(t=this._checkBadness())>=1)1&n&&(s[r-1-e+8*r]=1,e<6?s[8+r*e]=1:s[8+r*(e+1)]=1);for(e=0;e<7;e++,n>>=1)1&n&&(s[8+r*(r-7+e)]=1,e?s[6-e+8*r]=1:s[7+8*r]=1)},_interleaveBlocks:function(){var t,e,i=this._dataBlock,n=this._ecc,s=this._eccBlock,r=0,o=this._calculateMaxLength(),a=this._neccBlock1,h=this._neccBlock2,f=this._stringBuffer;for(t=0;t1)for(t=u.BLOCK[n],i=s-7;;){for(e=s-7;e>t-3&&(this._addAlignment(e,i),!(e6)for(t=d.BLOCK[r-7],e=17,i=0;i<6;i++)for(n=0;n<3;n++,e--)1&(e>11?r>>e-12:t>>e)?(s[5-i+o*(2-n+o-11)]=1,s[2-n+o-11+o*(5-i)]=1):(this._setMask(5-i,2-n+o-11),this._setMask(2-n+o-11,5-i))},_isMasked:function(t,e){var i=v._getMaskBit(t,e);return 1===this._mask[i]},_pack:function(){var t,e,i,n=1,s=1,r=this.width,o=r-1,a=r-1,h=(this._dataBlock+this._eccBlock)*(this._neccBlock1+this._neccBlock2)+this._neccBlock2;for(e=0;ee&&(i=t,t=e,e=i),i=e,i+=e*e,i>>=1,i+=t},_modN:function(t){for(;t>=255;)t=((t-=255)>>8)+(255&t);return t},N1:3,N2:3,N3:40,N4:10}),p=v,m=f.extend({draw:function(){this.element.src=this.qrious.toDataURL()},reset:function(){this.element.src=""},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),g=h.extend(function(t,e,i,n){this.name=t,this.modifiable=Boolean(e),this.defaultValue=i,this._valueTransformer=n},{transform:function(t){var e=this._valueTransformer;return"function"==typeof e?e(t,this):t}}),k=h.extend(null,{abs:function(t){return null!=t?Math.abs(t):null},hasOwn:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},noop:function(){},toUpperCase:function(t){return null!=t?t.toUpperCase():null}}),w=h.extend(function(t){this.options={},t.forEach(function(t){this.options[t.name]=t},this)},{exists:function(t){return null!=this.options[t]},get:function(t,e){return w._get(this.options[t],e)},getAll:function(t){var e,i=this.options,n={};for(e in i)k.hasOwn(i,e)&&(n[e]=w._get(i[e],t));return n},init:function(t,e,i){"function"!=typeof i&&(i=k.noop);var n,s;for(n in this.options)k.hasOwn(this.options,n)&&(s=this.options[n],w._set(s,s.defaultValue,e),w._createAccessor(s,e,i));this._setAll(t,e,!0)},set:function(t,e,i){return this._set(t,e,i)},setAll:function(t,e){return this._setAll(t,e)},_set:function(t,e,i,n){var s=this.options[t];if(!s)throw new Error("Invalid option: "+t);if(!s.modifiable&&!n)throw new Error("Option cannot be modified: "+t);return w._set(s,e,i)},_setAll:function(t,e,i){if(!t)return!1;var n,s=!1;for(n in t)k.hasOwn(t,n)&&this._set(n,t[n],e,i)&&(s=!0);return s}},{_createAccessor:function(t,e,i){var n={get:function(){return w._get(t,e)}};t.modifiable&&(n.set=function(n){w._set(t,n,e)&&i(n,t)}),Object.defineProperty(e,t.name,n)},_get:function(t,e){return e["_"+t.name]},_set:function(t,e,i){var n="_"+t.name,s=i[n],r=t.transform(null!=e?e:t.defaultValue);return i[n]=r,r!==s}}),M=w,b=h.extend(function(){this._services={}},{getService:function(t){var e=this._services[t];if(!e)throw new Error("Service is not being managed with name: "+t);return e},setService:function(t,e){if(this._services[t])throw new Error("Service is already managed with name: "+t);e&&(this._services[t]=e)}}),B=new M([new g("background",!0,"white"),new g("backgroundAlpha",!0,1,k.abs),new g("element"),new g("foreground",!0,"black"),new g("foregroundAlpha",!0,1,k.abs),new g("level",!0,"L",k.toUpperCase),new g("mime",!0,"image/png"),new g("padding",!0,null,k.abs),new g("size",!0,100,k.abs),new g("value",!0,"")]),y=new b,O=h.extend(function(t){B.init(t,this,this.update.bind(this));var e=B.get("element",this),i=y.getService("element"),n=e&&i.isCanvas(e)?e:i.createCanvas(),s=e&&i.isImage(e)?e:i.createImage();this._canvasRenderer=new c(this,n,!0),this._imageRenderer=new m(this,s,s===e),this.update()},{get:function(){return B.getAll(this)},set:function(t){B.setAll(t,this)&&this.update()},toDataURL:function(t){return this.canvas.toDataURL(t||this.mime)},update:function(){var t=new p({level:this.level,value:this.value});this._canvasRenderer.render(t),this._imageRenderer.render(t)}},{use:function(t){y.setService(t.getName(),t)}});Object.defineProperties(O.prototype,{canvas:{get:function(){return this._canvasRenderer.getElement()}},image:{get:function(){return this._imageRenderer.getElement()}}});var A=O,L=h.extend({getName:function(){}}).extend({createCanvas:function(){},createImage:function(){},getName:function(){return"element"},isCanvas:function(t){},isImage:function(t){}}).extend({createCanvas:function(){return document.createElement("canvas")},createImage:function(){return document.createElement("img")},isCanvas:function(t){return t instanceof HTMLCanvasElement},isImage:function(t){return t instanceof HTMLImageElement}});return A.use(new L),A}); + +//# sourceMappingURL=qrious.min.js.map \ No newline at end of file diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index afbfc8656f..85b12b980d 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -157,12 +157,36 @@ login.login_handlers = (function() { var login_handlers = { 200: function(data) { console.log(data); - if(data.token) { + if(data.verification) { login.set_indicator("{{ _("Success") }}", 'green'); - $('.login-content').empty().append($('
').html('
\ + $('.login-content').empty().append($('
').attr({'id':'otp_div'}).html('
\ Verification
\ \ ')); + if (!data.verification.setup_completed){ + var qrcode = $('
').attr('id','qrcode_div'); + var direction = $('
').attr('id','qr_info').text('Scan QR Code and enter the resulting code displayed'); + var qrcanvas = $(''); + 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 = $('
').attr('id','qrcode_div'); + var direction = $('
').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) + } document.cookie = "tmp_id="+data.tmp_id; verify_token(); return false; From 33f416801fb1718309e082610de3c1eb684c0f60 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Fri, 30 Jun 2017 17:39:05 +0100 Subject: [PATCH 03/67] enable 2fa from system settings --- frappe/auth.py | 80 ++++++++++--------- .../system_settings/system_settings.json | 35 +++++++- frappe/core/doctype/user/user.json | 33 +++++++- frappe/core/doctype/user/user.py | 2 + 4 files changed, 110 insertions(+), 40 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 94241a8e34..9a387666d9 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -118,48 +118,51 @@ class LoginManager: def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) - otp = frappe.form_dict.get('otp') - if not otp: + + if frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') == unicode(0): self.authenticate() - # 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: + self.post_login(no_two_auth=True) + else: + otp = frappe.form_dict.get('otp') + if not otp: + self.authenticate() + # 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') + else: + import os + import base64 + 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") + frappe.local.response['verification'] = {'setup_completed':False, 'totp_uri':totp_uri} + + tmp_id = frappe.generate_hash(length=8) + usr = frappe.form_dict.get('usr') + pwd = frappe.form_dict.get('pwd') + frappe.cache().hset('token',tmp_id,{'usr':usr,'pwd':pwd,'otp_secret':otp_secret}) + frappe.local.response['tmp_id'] = tmp_id + + raise frappe.RequestToken - if user_info.two_factor_setup: - frappe.local.response['verification'] = {'setup_completed':True} - otp_secret = frappe.db.get_default(self.user + '_otpsecret') else: - import os - import base64 - otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') - frappe.db.set_default(self.user + '_otpsecret', otp_secret) - # set two_factor_setup as 1 meaning user has copied otpsecret - frappe.db.set_value("User", self.user, 'two_factor_setup', 1) - frappe.db.commit() - 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} - - tmp_id = frappe.generate_hash(length=8) - usr = frappe.form_dict.get('usr') - pwd = frappe.form_dict.get('pwd') - frappe.cache().hset('token',tmp_id,{'usr':usr,'pwd':pwd,'otp_secret':otp_secret}) - frappe.local.response['tmp_id'] = tmp_id - - raise frappe.RequestToken + self.post_login(no_two_auth=True) else: - self.post_login(no_two_auth=True) - - else: - try: - tmp_info = frappe.cache().hget('token', frappe.form_dict.get('tmp_id')) - self.authenticate(user=tmp_info['usr'], pwd=tmp_info['pwd']) - except: - frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") - #frappe.respond_as_web_page("Logged Out", """

You have been logged out.

Back to Home

""") - #frappe.throw("+++++ YOUR LOGIN WAS SUCCESSFUL, CONGRATS +++++") - #frappe.website.render('/404.html') - self.post_login() + try: + tmp_info = frappe.cache().hget('token', frappe.form_dict.get('tmp_id')) + self.authenticate(user=tmp_info['usr'], pwd=tmp_info['pwd']) + except: + frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") + #frappe.respond_as_web_page("Logged Out", """

You have been logged out.

Back to Home

""") + #frappe.throw("+++++ YOUR LOGIN WAS SUCCESSFUL, CONGRATS +++++") + #frappe.website.render('/404.html') + self.post_login() def post_login(self,no_two_auth=False): self.run_trigger('on_login') @@ -181,6 +184,9 @@ class LoginManager: totp = pyotp.TOTP(otp_secret) if totp.verify(otp): frappe.cache().hdel('token', tmp_id) + # show qr code only once + frappe.db.set_value("User", self.user, 'two_factor_setup', 1) + frappe.db.commit() return True else: self.fail('Incorrect Verification code', user=frappe.cache().hget('token',tmp_id).get('usr')) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index b5cf055ae7..240da9b303 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -679,6 +679,37 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "enable_two_factor_auth", + "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": "Enable 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, @@ -965,7 +996,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-06-12 13:05:28.924098", + "modified": "2017-06-29 18:01:46.292635", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -1000,4 +1031,4 @@ "sort_order": "ASC", "track_changes": 1, "track_seen": 0 -} +} \ No newline at end of file diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 809a9ea2e7..37449fd072 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1723,6 +1723,37 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "two_factor_method", + "fieldtype": "Select", + "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 Method", + "length": 0, + "no_copy": 0, + "options": "OTP App\nSMS", + "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, @@ -2033,7 +2064,7 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-06-28 14:40:26.616254", + "modified": "2017-06-30 16:26:06.481438", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 321b4f9304..9ea6f4cccd 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -154,6 +154,8 @@ class User(Document): and name in ({0}) limit 1""".format(', '.join(['%s'] * len(self.roles))), [d.role for d in self.roles]))): self.two_factor_auth = 1 + else: + self.two_factor_auth = 0 def has_desk_access(self): '''Return true if any of the set roles has desk access''' From 9741ca7dcfe348d70cd83e93ede76bef2cd47bd1 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Thu, 6 Jul 2017 18:46:26 +0100 Subject: [PATCH 04/67] use OTP App, SMS or Email to authenticate --- frappe/auth.py | 155 ++++++++++-- frappe/core/doctype/role/role.json | 4 +- .../system_settings/system_settings.json | 35 ++- frappe/core/doctype/user/user.js | 22 ++ frappe/core/doctype/user/user.json | 69 +----- frappe/core/doctype/user/user.py | 99 +++++++- frappe/templates/includes/login/login.js | 222 ++++++++++++++++-- frappe/www/login.py | 15 ++ 8 files changed, 507 insertions(+), 114 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 9a387666d9..1b43096014 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -17,6 +17,8 @@ from frappe.translate import get_lang_code from frappe.utils.password import check_password 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 import pyotp @@ -127,25 +129,90 @@ class LoginManager: if not otp: self.authenticate() # 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: import os import base64 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") - 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) usr = frappe.form_dict.get('usr') 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 raise frappe.RequestToken @@ -155,13 +222,14 @@ class LoginManager: else: 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']) except: frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") - #frappe.respond_as_web_page("Logged Out", """

You have been logged out.

Back to Home

""") - #frappe.throw("+++++ YOUR LOGIN WAS SUCCESSFUL, CONGRATS +++++") - #frappe.website.render('/404.html') + self.post_login() def post_login(self,no_two_auth=False): @@ -169,27 +237,42 @@ class LoginManager: self.validate_ip_address() self.validate_hour() 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.set_user_info() else: self.make_session() 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: - 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: 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) if totp.verify(otp): - frappe.cache().hdel('token', tmp_id) # 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 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): # set sid again @@ -333,6 +416,35 @@ class LoginManager: def clear_cookies(self): 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='

Your verification code is {}

'.format(hotp.at(int(token))),delayed=False, retry=3) + return True + class CookieManager: def __init__(self): self.cookies = {} @@ -367,6 +479,7 @@ class CookieManager: for key in set(self.to_delete): response.set_cookie(key, "", expires=expires) + @frappe.whitelist() def get_logged_user(): return frappe.session.user diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index a97def1910..1eebb71a36 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -121,7 +121,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Two Factor Authenticaction", + "label": "Two Factor Authentication", "length": 0, "no_copy": 0, "permlevel": 0, @@ -179,7 +179,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-28 13:29:49.915545", + "modified": "2017-07-06 12:42:57.097914", "modified_by": "Administrator", "module": "Core", "name": "Role", diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 240da9b303..33130389f3 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -710,6 +710,39 @@ "set_only_once": 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_on_submit": 0, @@ -996,7 +1029,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-06-29 18:01:46.292635", + "modified": "2017-07-06 14:44:04.601775", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index aa7f7940e3..14918a8c8a 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -107,6 +107,28 @@ frappe.ui.form.on('User', { } 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) { if(frm.roles_editor) { diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 37449fd072..6d809a9292 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1729,9 +1729,10 @@ "bold": 0, "collapsible": 0, "columns": 0, + "default": "OTP App", "fieldname": "two_factor_method", "fieldtype": "Select", - "hidden": 1, + "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, @@ -1741,7 +1742,7 @@ "label": "Two Factor Authentication Method", "length": 0, "no_copy": 0, - "options": "OTP App\nSMS", + "options": "OTP App\nSMS\nEmail", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1987,68 +1988,6 @@ "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_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, @@ -2064,7 +2003,7 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-06-30 16:26:06.481438", + "modified": "2017-07-04 15:53:25.877843", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 9ea6f4cccd..5b4679a486 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -489,6 +489,38 @@ class User(Document): if len(email_accounts) != len(set(email_accounts)): 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() def get_timezones(): import pytz @@ -903,4 +935,69 @@ def handle_password_test_fail(result): def update_gravatar(name): gravatar = has_gravatar(name) if gravatar: - frappe.db.set_value('User', name, 'user_image', gravatar) \ No newline at end of file + 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='

Your verification code is {0}

'.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() \ No newline at end of file diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 85b12b980d..f5e0f860a0 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -159,37 +159,211 @@ login.login_handlers = (function() { console.log(data); if(data.verification) { login.set_indicator("{{ _("Success") }}", 'green'); - $('.login-content').empty().append($('
').attr({'id':'otp_div'}).html('
\ + + var continue_otp = function(setup_completed,method_prompt){ + + $('.login-content').empty().append($('
').attr({'id':'otp_div'}).html('
\ Verification
\ - \ + \ ')); - if (!data.verification.setup_completed){ - var qrcode = $('
').attr('id','qrcode_div'); - var direction = $('
').attr('id','qr_info').text('Scan QR Code and enter the resulting code displayed'); - var qrcanvas = $(''); - 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 = $('
').attr('id','qrcode_div'); + + var direction = $('
').attr('id','qr_info').text(method_prompt || 'Scan QR Code and enter the resulting code displayed'); + + var qrcanvas = $(''); + 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 = $('
').attr('id','qrcode_div'); + var direction = $('
').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($('
').attr({'id':'otp_div'}).html( + '
\ +
\ + Verification\ +
\ + \ + \ +
')); + + verify_token(); + + if (!setup_completed){ + var sms_div = $('
').attr({'id':'sms_div','style':'margin-bottom: 20px;'}); + var direction = $('
').attr({'id':'sms_info','style':'margin-bottom: 15px;'}).text('Enter phone number to send verification code'); + sms_div.append(direction); + sms_div.append($('
').attr({'id':'sms_code_div'}).html( + '
\ + \ + \ +

')); + + $('#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( + '

SMS sent.
Enter verification code received


' + ); + } else { + $('#sms_div').empty().append( + '

SMS not sent


' + ); + } + } }); + }) + } else { + var smscode = $('
').attr('id','smscode_div'); + var direction = $('
').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($('
').attr({'id':'otp_div'}).html( + '
\ +
\ + Verification\ +
\ + \ + \ +
')); + + verify_token(); + + if (!setup_completed){ + var email_div = $('
').attr({'id':'email_div','style':'margin-bottom: 20px;'}); + email_div.append('

Verification code email will be sent to registered email address. Enter code received below

') + + $('#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( + '

Email not sent


' + ); + } + } + }); + } else { + if (method_prompt){ + var emailcode = $('
').attr('id','emailcode_div'); + var direction = $('
').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 = $('
').attr('id','emailcode_div'); + var direction = $('
').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('
\ +
\ +

Select verification Method
\ + method may be changed later in settings

\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ + \ +
\ + \ +
') + + 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 { - var qrcode = $('
').attr('id','qrcode_div'); - var direction = $('
').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; - verify_token(); + //verify_token(); return false; + } else if(data.message == 'Logged In'){ login.set_indicator("{{ _("Success") }}", 'green'); window.location.href = get_url_arg("redirect-to") || data.home_page; diff --git a/frappe/www/login.py b/frappe/www/login.py index dbfc6f6fe5..e81c7bcb78 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -12,6 +12,20 @@ from frappe.integrations.doctype.ldap_settings.ldap_settings import get_ldap_set 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): if frappe.session.user != "Guest" and frappe.session.data.user_type=="System User": frappe.local.flags.redirect_location = "/testpayment" @@ -30,6 +44,7 @@ def get_context(context): ldap_settings = get_ldap_settings() context["ldap_settings"] = ldap_settings + context['qqrcode'] = frappe.render_template(get_qr_code()) return context From fbd8218dffca73f3714b87a437e4f000dd6540e8 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Fri, 7 Jul 2017 12:27:29 +0100 Subject: [PATCH 05/67] fix otp method to default in User doctype --- frappe/auth.py | 19 ++++++++++--------- frappe/core/doctype/user/user.json | 6 +++--- frappe/core/doctype/user/user.py | 11 ----------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 1b43096014..a18d36cbf7 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -139,13 +139,14 @@ class LoginManager: restrict_method = frappe.db.get_value('System Settings', None, 'fix_2fa_method') verification_meth = frappe.db.get_value('User', self.user, 'two_factor_method') + fixed_method = [frappe._dict()] - if restrict_method: + if int(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()] + pass if not verification_meth: verification_method = fixed_method[0].default_method or 'OTP App' @@ -167,7 +168,7 @@ class LoginManager: elif verification_method == 'OTP App': totp_uri = False - if frappe.db.get_default(self.user + '_otpsecret', otp_secret): + if frappe.db.get_default(self.user + '_otplogin'): totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") verification_obj = {'token_delivery': True, @@ -194,7 +195,7 @@ class LoginManager: 'token_delivery': True, 'prompt': False, 'totp_uri': totp_uri, - 'restrict_method': fixed_method[0].default_method or 'OTP App' + 'restrict_method': int(restrict_method) and (fixed_method[0].default_method or 'OTP App') } tmp_id = frappe.generate_hash(length=8) @@ -211,7 +212,7 @@ class LoginManager: 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.cache().expire(field,180) frappe.local.response['tmp_id'] = tmp_id @@ -228,8 +229,8 @@ class LoginManager: } self.authenticate(user=tmp_info['usr'], pwd=tmp_info['pwd']) except: - frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") - + pass + # frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") self.post_login() def post_login(self,no_two_auth=False): @@ -247,9 +248,9 @@ class LoginManager: def confirm_token(self,otp=None, tmp_id=None, hotp_token=False): try: - otp_secret = frappe.cache().get(tmp_id + '_otp_secret') or frappe.db.get_default(self.user + '_otpsecret') + otp_secret = frappe.cache().get(tmp_id + '_otp_secret') if not otp_secret: - return False + frappe.throw('Login session expired, please refresh page to try again') except AttributeError: return False diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 6d809a9292..f5ce17080c 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1729,7 +1729,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "OTP App", + "default": "SMS", "fieldname": "two_factor_method", "fieldtype": "Select", "hidden": 0, @@ -2003,8 +2003,8 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-07-04 15:53:25.877843", - "modified_by": "Administrator", + "modified": "2017-07-07 11:31:54.900879", + "modified_by": "crossxcell99@gmail.com", "module": "Core", "name": "User", "owner": "Administrator", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5b4679a486..0a04432824 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -57,7 +57,6 @@ class User(Document): self.validate_email_type(self.name) self.add_system_manager_role() self.set_system_user() - self.set_two_factor_auth() self.set_full_name() self.check_enable_disable() self.ensure_unique_roles() @@ -147,16 +146,6 @@ class User(Document): else: self.user_type = 'Website User' - def set_two_factor_auth(self): - '''Set two factor authentication for user''' - if (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]))): - self.two_factor_auth = 1 - else: - self.two_factor_auth = 0 - def has_desk_access(self): '''Return true if any of the set roles has desk access''' if not self.roles: From e4c2057accc52ddaaa417671758cc07fe9086fae Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Fri, 7 Jul 2017 17:26:52 +0100 Subject: [PATCH 06/67] general verification method set in system settings --- frappe/auth.py | 34 +++--- .../system_settings/system_settings.json | 13 +- frappe/core/doctype/user/user.js | 36 +++--- frappe/core/doctype/user/user.json | 36 +----- frappe/core/doctype/user/user.py | 58 ++++----- frappe/templates/includes/login/login.js | 113 +++++++++--------- 6 files changed, 130 insertions(+), 160 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index a18d36cbf7..39578fead8 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -137,21 +137,22 @@ class LoginManager: 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') - fixed_method = [frappe._dict()] - - if int(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: - pass - - 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 + #restrict_method = frappe.db.get_value('System Settings', None, 'fix_2fa_method') + #verification_meth = frappe.db.get_value('User', self.user, 'two_factor_method') + #fixed_method = [frappe._dict()] + + #if int(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: + # pass + + #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 + verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') if otp_secret: @@ -192,10 +193,11 @@ class LoginManager: frappe.local.response['verification'] = { 'method_first_time': True, + 'method': verification_method, 'token_delivery': True, 'prompt': False, 'totp_uri': totp_uri, - 'restrict_method': int(restrict_method) and (fixed_method[0].default_method or 'OTP App') + #'restrict_method': int(restrict_method) and (fixed_method[0].default_method or 'OTP App') } tmp_id = frappe.generate_hash(length=8) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 33130389f3..6649aad4f2 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -716,11 +716,11 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "0", + "default": "OTP App", "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", + "description": "Choose authentication method to be used by all users", + "fieldname": "two_factor_method", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -728,9 +728,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Fix authentication method", + "label": "Authentication method", "length": 0, "no_copy": 0, + "options": "OTP App\nSMS\nEmail", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1029,7 +1030,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-06 14:44:04.601775", + "modified": "2017-07-07 17:21:50.082744", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 14918a8c8a..39423ae600 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -108,27 +108,27 @@ frappe.ui.form.on('User', { 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') - } - }); + // 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) { if(frm.roles_editor) { diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index f5ce17080c..aca7bcab3d 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1723,38 +1723,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "SMS", - "fieldname": "two_factor_method", - "fieldtype": "Select", - "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 Authentication Method", - "length": 0, - "no_copy": 0, - "options": "OTP App\nSMS\nEmail", - "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, @@ -2003,8 +1971,8 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-07-07 11:31:54.900879", - "modified_by": "crossxcell99@gmail.com", + "modified": "2017-07-07 17:18:14.047969", + "modified_by": "Administrator", "module": "Core", "name": "User", "owner": "Administrator", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 0a04432824..4ad45849fd 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -478,28 +478,28 @@ class User(Document): if len(email_accounts) != len(set(email_accounts)): 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} +# 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': @@ -983,10 +983,10 @@ def send_token_via_email(tmp_id,token=None): message='

Your verification code is {0}

'.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() \ No newline at end of file +#@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() \ No newline at end of file diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index f5e0f860a0..997e058c6e 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -162,10 +162,14 @@ login.login_handlers = (function() { var continue_otp = function(setup_completed,method_prompt){ - $('.login-content').empty().append($('
').attr({'id':'otp_div'}).html('
\ - Verification
\ - \ -
')); + $('.login-content').empty().append($('
').attr({'id':'otp_div'}).html( + '
\ +
\ + Verification\ +
\ + \ + \ +
')); verify_token(); @@ -299,55 +303,54 @@ login.login_handlers = (function() { } if (data.verification.method_first_time){ - $('.login-content').empty().append('
\ -
\ -

Select verification Method
\ - method may be changed later in settings

\ -
\ -
\ - \ -
\ -
\ - \ -
\ -
\ - \ -
\ - \ -
') - - if (data.verification.restrict_method){ - $('input[name=method]').each(function(){ - if ($(this).val() != data.verification.restrict_method){ - $(this).attr('disabled',true) - } - }) + // $('.login-content').empty().append('
\ + //
\ + //

Select verification Method
\ + // method may be changed later in settings

\ + //
\ + //
\ + // \ + //
\ + //
\ + // \ + //
\ + //
\ + // \ + //
\ + // \ + //
') + + // 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 (data.verification.method == 'OTP App'){ + continue_otp(setup_completed=false); + } else if (data.verification.method == 'SMS'){ + continue_sms(setup_completed=false); + } else if (data.verification.method == 'Email'){ + continue_email(setup_completed=false); } - $('#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) { } - }); - }); + // 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 { if (data.verification.method == 'OTP App'){ console.log(data.verification.totp_uri) @@ -430,20 +433,16 @@ frappe.ready(function() { }); var verify_token = function(event) { - $('#verify_token').bind("click", function() { - console.log("Why XX2"); - //eventx.preventDefault(); + $(".form-verify").on("submit", function(eventx) { + eventx.preventDefault(); var args = {}; args.cmd = "login"; args.otp = $("#login_token").val(); - console.log("LLLLLLLLLLLLLLLLLLL"); args.tmp_id = frappe.get_cookie('tmp_id'); if(!args.otp) { frappe.msgprint('{{ _("Login token required") }}'); return false; } - console.log("Button Clicked") - console.log(args) login.call(args); return false; }); From 73d772604bbbcff876266104c6d346173067bdb8 Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Wed, 12 Jul 2017 23:48:04 +0100 Subject: [PATCH 07/67] Added pyotp to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e0d7ab4b3d..2afcee334e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,3 +42,4 @@ oauthlib PyJWT pypdf openpyxl +pyotp From fd3f0ced7a584a9b77ee3c8ff05480f59b71a173 Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Sun, 16 Jul 2017 15:28:36 +0100 Subject: [PATCH 08/67] Include pyqrcode in requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 872743fdef..7f099d6ddf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,3 +42,4 @@ PyJWT pypdf openpyxl pyotp +pyqrcode From 4b84a1a5728b3faabbba1d126fc7766cab6421cb Mon Sep 17 00:00:00 2001 From: B H Boma Date: Tue, 18 Jul 2017 10:39:44 +0100 Subject: [PATCH 09/67] [fix] Qrcode not visible for twofactor auth --- frappe/auth.py | 47 +++++++++++++----------- frappe/public/build.json | 3 -- frappe/templates/includes/login/login.js | 21 ++++------- frappe/www/login.py | 18 +-------- 4 files changed, 34 insertions(+), 55 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 39578fead8..7cb903e30e 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -17,7 +17,6 @@ from frappe.translate import get_lang_code from frappe.utils.password import check_password 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 @@ -130,28 +129,16 @@ class LoginManager: self.authenticate() # after authenticate, self.user is set (from check_password() call) 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])) + two_factor_auth_user = 0 + if user_obj.roles: + query = """select name from `tabRole` where two_factor_auth=1 + and name in ("All"{0}) limit 1""".format(', '.join('\"{}\"'.format(i.role) for \ + i in user_obj.roles)) + two_factor_auth_user = len(frappe.db.sql(query)) + 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') - #fixed_method = [frappe._dict()] - - #if int(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: - # pass - - #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 verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') if otp_secret: @@ -175,7 +162,8 @@ class LoginManager: verification_obj = {'token_delivery': True, 'prompt': False, 'totp_uri': totp_uri, - 'method': 'OTP App'} + 'method': 'OTP App', + 'qrcode':get_qr_svg_code(totp_uri)} elif verification_method == 'Email': status = self.send_token_via_email(token=token,otpsecret=otp_secret) verification_obj = {'token_delivery': status, @@ -197,6 +185,7 @@ class LoginManager: 'token_delivery': True, 'prompt': False, 'totp_uri': totp_uri, + 'qrcode':get_qr_svg_code(totp_uri) #'restrict_method': int(restrict_method) and (fixed_method[0].default_method or 'OTP App') } @@ -420,6 +409,10 @@ class LoginManager: clear_cookies() def send_token_via_sms(self,otpsecret,token=None,phone_no=None): + try: + from erpnext.setup.doctype.sms_settings.sms_settings import send_request + except: + return False ss = frappe.get_doc('SMS Settings', 'SMS Settings') if not ss.sms_gateway_url: return False @@ -499,3 +492,15 @@ def get_website_user_home_page(user): return '/' + home_page.strip('/') else: return '/me' + +def get_qr_svg_code(totp_uri): + '''Get SVG code to display Qrcode for OTP.''' + from pyqrcode import create as qrcreate + from StringIO import StringIO + from base64 import b64encode + url = qrcreate(totp_uri) + stream = StringIO() + url.svg(stream, scale=5) + svg = stream.getvalue().replace('\n','') + svg = b64encode(bytes(svg)) + return svg diff --git a/frappe/public/build.json b/frappe/public/build.json index 082fbf5a71..75e4e76469 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -22,9 +22,6 @@ "website/js/website.js", "public/js/frappe/misc/rating_icons.html" ], - "js/qrious.min.js": [ - "public/js/frappe/qrious.min.js" - ], "js/dialog.min.js": [ "public/js/frappe/dom.js", "public/js/frappe/ui/modal.html", diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 997e058c6e..d460a4e62a 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -174,24 +174,17 @@ login.login_handlers = (function() { verify_token(); if (!setup_completed){ - var qrcode = $('
').attr('id','qrcode_div'); + var qrcode = $('
') + qrcode.attr('id','qrcode_div'); + qrcode.css('text-align','center'); var direction = $('
').attr('id','qr_info').text(method_prompt || 'Scan QR Code and enter the resulting code displayed'); - var qrcanvas = $(''); - qrcanvas.attr('id','qrcanvass'); + var qrimg = $(''); + qrimg.attr('src','data:image/svg+xml;base64,' + data.verification.qrcode); 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 - }); + qrcode.append(qrimg); + $('#otp_div').prepend(qrcode); } else { var qrcode = $('
').attr('id','qrcode_div'); var direction = $('
').attr('id','qr_info').text(method_prompt || 'Enter Code displayed in OTP App'); diff --git a/frappe/www/login.py b/frappe/www/login.py index e81c7bcb78..5002a44b35 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -12,23 +12,9 @@ from frappe.integrations.doctype.ldap_settings.ldap_settings import get_ldap_set 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): if frappe.session.user != "Guest" and frappe.session.data.user_type=="System User": - frappe.local.flags.redirect_location = "/testpayment" + frappe.local.flags.redirect_location = "/desk" raise frappe.Redirect # get settings from site config @@ -44,7 +30,6 @@ def get_context(context): ldap_settings = get_ldap_settings() context["ldap_settings"] = ldap_settings - context['qqrcode'] = frappe.render_template(get_qr_code()) return context @@ -83,4 +68,3 @@ def login_via_token(login_token): frappe.local.login_manager = LoginManager() redirect_post_login(desk_user = frappe.db.get_value("User", frappe.session.user, "user_type")=="System User") - From f34d52fb654adbd4f28c8251176b753ae268b605 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Tue, 18 Jul 2017 17:05:24 +0100 Subject: [PATCH 10/67] Minor fix for checking successful otp login --- frappe/auth.py | 14 ++++++++++---- frappe/public/js/frappe/qrious.min.js | 2 +- frappe/templates/includes/login/login.js | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 7cb903e30e..50d7f04b93 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -154,16 +154,20 @@ class LoginManager: '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 + + totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") if frappe.db.get_default(self.user + '_otplogin'): - totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") + otp_setup_completed = True + else: + otp_setup_completed = False verification_obj = {'token_delivery': True, 'prompt': False, 'totp_uri': totp_uri, 'method': 'OTP App', - 'qrcode':get_qr_svg_code(totp_uri)} + 'qrcode': get_qr_svg_code(totp_uri), + 'otp_setup_completed': otp_setup_completed} elif verification_method == 'Email': status = self.send_token_via_email(token=token,otpsecret=otp_secret) verification_obj = {'token_delivery': status, @@ -185,7 +189,8 @@ class LoginManager: 'token_delivery': True, 'prompt': False, 'totp_uri': totp_uri, - 'qrcode':get_qr_svg_code(totp_uri) + 'qrcode':get_qr_svg_code(totp_uri), + 'otp_setup_completed': False, #'restrict_method': int(restrict_method) and (fixed_method[0].default_method or 'OTP App') } @@ -261,6 +266,7 @@ class LoginManager: # show qr code only once if not frappe.db.get_default(self.user + '_otpsecret'): frappe.db.set_default(self.user + '_otpsecret', otp_secret) + if not frappe.db.get_default(self.user + '_otplogin'): frappe.db.set_default(self.user + '_otplogin', 1) return True else: diff --git a/frappe/public/js/frappe/qrious.min.js b/frappe/public/js/frappe/qrious.min.js index 5735ea62de..5943f8c0f9 100644 --- a/frappe/public/js/frappe/qrious.min.js +++ b/frappe/public/js/frappe/qrious.min.js @@ -1,6 +1,6 @@ /*! QRious v4.0.2 | (C) 2017 Alasdair Mercer | GPL v3 License Based on jsqrencode | (C) 2010 tz@execpc.com | GPL v3 License */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.QRious=e()}(this,function(){"use strict";function t(t,e){var n;return"function"==typeof Object.create?n=Object.create(t):(s.prototype=t,n=new s,s.prototype=null),e&&i(!0,n,e),n}function e(e,n,s,r){var o=this;return"string"!=typeof e&&(r=s,s=n,n=e,e=null),"function"!=typeof n&&(r=s,s=n,n=function(){return o.apply(this,arguments)}),i(!1,n,o,r),n.prototype=t(o.prototype,s),n.prototype.constructor=n,n.class_=e||o.class_,n.super_=o,n}function i(t,e,i){for(var n,s,a=0,h=(i=o.call(arguments,2)).length;a>1&1,n=0;n0;e--)n[e]=n[e]?n[e-1]^_.EXPONENT[v._modN(_.LOG[n[e]]+t)]:n[e-1];n[0]=_.EXPONENT[v._modN(_.LOG[n[0]]+t)]}for(t=0;t<=i;t++)n[t]=_.LOG[n[t]]},_checkBadness:function(){var t,e,i,n,s,r=0,o=this._badness,a=this.buffer,h=this.width;for(s=0;sh*h;)u-=h*h,c++;for(r+=c*v.N4,n=0;n=o-2&&(t=o-2,s>9&&t--);var a=t;if(s>9){for(r[a+2]=0,r[a+3]=0;a--;)e=r[a],r[a+3]|=255&e<<4,r[a+2]=e>>4;r[2]|=255&t<<4,r[1]=t>>4,r[0]=64|t>>12}else{for(r[a+1]=0,r[a+2]=0;a--;)e=r[a],r[a+2]|=255&e<<4,r[a+1]=e>>4;r[1]|=255&t<<4,r[0]=64|t>>4}for(a=t+3-(s<10);a=5&&(i+=v.N1+n[e]-5);for(e=3;et||3*n[e-3]>=4*n[e]||3*n[e+3]>=4*n[e])&&(i+=v.N3);return i},_finish:function(){this._stringBuffer=this.buffer.slice();var t,e,i=0,n=3e4;for(e=0;e<8&&(this._applyMask(e),(t=this._checkBadness())>=1)1&n&&(s[r-1-e+8*r]=1,e<6?s[8+r*e]=1:s[8+r*(e+1)]=1);for(e=0;e<7;e++,n>>=1)1&n&&(s[8+r*(r-7+e)]=1,e?s[6-e+8*r]=1:s[7+8*r]=1)},_interleaveBlocks:function(){var t,e,i=this._dataBlock,n=this._ecc,s=this._eccBlock,r=0,o=this._calculateMaxLength(),a=this._neccBlock1,h=this._neccBlock2,f=this._stringBuffer;for(t=0;t1)for(t=u.BLOCK[n],i=s-7;;){for(e=s-7;e>t-3&&(this._addAlignment(e,i),!(e6)for(t=d.BLOCK[r-7],e=17,i=0;i<6;i++)for(n=0;n<3;n++,e--)1&(e>11?r>>e-12:t>>e)?(s[5-i+o*(2-n+o-11)]=1,s[2-n+o-11+o*(5-i)]=1):(this._setMask(5-i,2-n+o-11),this._setMask(2-n+o-11,5-i))},_isMasked:function(t,e){var i=v._getMaskBit(t,e);return 1===this._mask[i]},_pack:function(){var t,e,i,n=1,s=1,r=this.width,o=r-1,a=r-1,h=(this._dataBlock+this._eccBlock)*(this._neccBlock1+this._neccBlock2)+this._neccBlock2;for(e=0;ee&&(i=t,t=e,e=i),i=e,i+=e*e,i>>=1,i+=t},_modN:function(t){for(;t>=255;)t=((t-=255)>>8)+(255&t);return t},N1:3,N2:3,N3:40,N4:10}),p=v,m=f.extend({draw:function(){this.element.src=this.qrious.toDataURL()},reset:function(){this.element.src=""},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),g=h.extend(function(t,e,i,n){this.name=t,this.modifiable=Boolean(e),this.defaultValue=i,this._valueTransformer=n},{transform:function(t){var e=this._valueTransformer;return"function"==typeof e?e(t,this):t}}),k=h.extend(null,{abs:function(t){return null!=t?Math.abs(t):null},hasOwn:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},noop:function(){},toUpperCase:function(t){return null!=t?t.toUpperCase():null}}),w=h.extend(function(t){this.options={},t.forEach(function(t){this.options[t.name]=t},this)},{exists:function(t){return null!=this.options[t]},get:function(t,e){return w._get(this.options[t],e)},getAll:function(t){var e,i=this.options,n={};for(e in i)k.hasOwn(i,e)&&(n[e]=w._get(i[e],t));return n},init:function(t,e,i){"function"!=typeof i&&(i=k.noop);var n,s;for(n in this.options)k.hasOwn(this.options,n)&&(s=this.options[n],w._set(s,s.defaultValue,e),w._createAccessor(s,e,i));this._setAll(t,e,!0)},set:function(t,e,i){return this._set(t,e,i)},setAll:function(t,e){return this._setAll(t,e)},_set:function(t,e,i,n){var s=this.options[t];if(!s)throw new Error("Invalid option: "+t);if(!s.modifiable&&!n)throw new Error("Option cannot be modified: "+t);return w._set(s,e,i)},_setAll:function(t,e,i){if(!t)return!1;var n,s=!1;for(n in t)k.hasOwn(t,n)&&this._set(n,t[n],e,i)&&(s=!0);return s}},{_createAccessor:function(t,e,i){var n={get:function(){return w._get(t,e)}};t.modifiable&&(n.set=function(n){w._set(t,n,e)&&i(n,t)}),Object.defineProperty(e,t.name,n)},_get:function(t,e){return e["_"+t.name]},_set:function(t,e,i){var n="_"+t.name,s=i[n],r=t.transform(null!=e?e:t.defaultValue);return i[n]=r,r!==s}}),M=w,b=h.extend(function(){this._services={}},{getService:function(t){var e=this._services[t];if(!e)throw new Error("Service is not being managed with name: "+t);return e},setService:function(t,e){if(this._services[t])throw new Error("Service is already managed with name: "+t);e&&(this._services[t]=e)}}),B=new M([new g("background",!0,"white"),new g("backgroundAlpha",!0,1,k.abs),new g("element"),new g("foreground",!0,"black"),new g("foregroundAlpha",!0,1,k.abs),new g("level",!0,"L",k.toUpperCase),new g("mime",!0,"image/png"),new g("padding",!0,null,k.abs),new g("size",!0,100,k.abs),new g("value",!0,"")]),y=new b,O=h.extend(function(t){B.init(t,this,this.update.bind(this));var e=B.get("element",this),i=y.getService("element"),n=e&&i.isCanvas(e)?e:i.createCanvas(),s=e&&i.isImage(e)?e:i.createImage();this._canvasRenderer=new c(this,n,!0),this._imageRenderer=new m(this,s,s===e),this.update()},{get:function(){return B.getAll(this)},set:function(t){B.setAll(t,this)&&this.update()},toDataURL:function(t){return this.canvas.toDataURL(t||this.mime)},update:function(){var t=new p({level:this.level,value:this.value});this._canvasRenderer.render(t),this._imageRenderer.render(t)}},{use:function(t){y.setService(t.getName(),t)}});Object.defineProperties(O.prototype,{canvas:{get:function(){return this._canvasRenderer.getElement()}},image:{get:function(){return this._imageRenderer.getElement()}}});var A=O,L=h.extend({getName:function(){}}).extend({createCanvas:function(){},createImage:function(){},getName:function(){return"element"},isCanvas:function(t){},isImage:function(t){}}).extend({createCanvas:function(){return document.createElement("canvas")},createImage:function(){return document.createElement("img")},isCanvas:function(t){return t instanceof HTMLCanvasElement},isImage:function(t){return t instanceof HTMLImageElement}});return A.use(new L),A}); +!function(t,e){console.log(t);console.log(e);"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.QRious=e()}(this,function(){"use strict";function t(t,e){var n;return"function"==typeof Object.create?n=Object.create(t):(s.prototype=t,n=new s,s.prototype=null),e&&i(!0,n,e),n}function e(e,n,s,r){var o=this;return"string"!=typeof e&&(r=s,s=n,n=e,e=null),"function"!=typeof n&&(r=s,s=n,n=function(){return o.apply(this,arguments)}),i(!1,n,o,r),n.prototype=t(o.prototype,s),n.prototype.constructor=n,n.class_=e||o.class_,n.super_=o,n}function i(t,e,i){for(var n,s,a=0,h=(i=o.call(arguments,2)).length;a>1&1,n=0;n0;e--)n[e]=n[e]?n[e-1]^_.EXPONENT[v._modN(_.LOG[n[e]]+t)]:n[e-1];n[0]=_.EXPONENT[v._modN(_.LOG[n[0]]+t)]}for(t=0;t<=i;t++)n[t]=_.LOG[n[t]]},_checkBadness:function(){var t,e,i,n,s,r=0,o=this._badness,a=this.buffer,h=this.width;for(s=0;sh*h;)u-=h*h,c++;for(r+=c*v.N4,n=0;n=o-2&&(t=o-2,s>9&&t--);var a=t;if(s>9){for(r[a+2]=0,r[a+3]=0;a--;)e=r[a],r[a+3]|=255&e<<4,r[a+2]=e>>4;r[2]|=255&t<<4,r[1]=t>>4,r[0]=64|t>>12}else{for(r[a+1]=0,r[a+2]=0;a--;)e=r[a],r[a+2]|=255&e<<4,r[a+1]=e>>4;r[1]|=255&t<<4,r[0]=64|t>>4}for(a=t+3-(s<10);a=5&&(i+=v.N1+n[e]-5);for(e=3;et||3*n[e-3]>=4*n[e]||3*n[e+3]>=4*n[e])&&(i+=v.N3);return i},_finish:function(){this._stringBuffer=this.buffer.slice();var t,e,i=0,n=3e4;for(e=0;e<8&&(this._applyMask(e),(t=this._checkBadness())>=1)1&n&&(s[r-1-e+8*r]=1,e<6?s[8+r*e]=1:s[8+r*(e+1)]=1);for(e=0;e<7;e++,n>>=1)1&n&&(s[8+r*(r-7+e)]=1,e?s[6-e+8*r]=1:s[7+8*r]=1)},_interleaveBlocks:function(){var t,e,i=this._dataBlock,n=this._ecc,s=this._eccBlock,r=0,o=this._calculateMaxLength(),a=this._neccBlock1,h=this._neccBlock2,f=this._stringBuffer;for(t=0;t1)for(t=u.BLOCK[n],i=s-7;;){for(e=s-7;e>t-3&&(this._addAlignment(e,i),!(e6)for(t=d.BLOCK[r-7],e=17,i=0;i<6;i++)for(n=0;n<3;n++,e--)1&(e>11?r>>e-12:t>>e)?(s[5-i+o*(2-n+o-11)]=1,s[2-n+o-11+o*(5-i)]=1):(this._setMask(5-i,2-n+o-11),this._setMask(2-n+o-11,5-i))},_isMasked:function(t,e){var i=v._getMaskBit(t,e);return 1===this._mask[i]},_pack:function(){var t,e,i,n=1,s=1,r=this.width,o=r-1,a=r-1,h=(this._dataBlock+this._eccBlock)*(this._neccBlock1+this._neccBlock2)+this._neccBlock2;for(e=0;ee&&(i=t,t=e,e=i),i=e,i+=e*e,i>>=1,i+=t},_modN:function(t){for(;t>=255;)t=((t-=255)>>8)+(255&t);return t},N1:3,N2:3,N3:40,N4:10}),p=v,m=f.extend({draw:function(){this.element.src=this.qrious.toDataURL()},reset:function(){this.element.src=""},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),g=h.extend(function(t,e,i,n){this.name=t,this.modifiable=Boolean(e),this.defaultValue=i,this._valueTransformer=n},{transform:function(t){var e=this._valueTransformer;return"function"==typeof e?e(t,this):t}}),k=h.extend(null,{abs:function(t){return null!=t?Math.abs(t):null},hasOwn:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},noop:function(){},toUpperCase:function(t){return null!=t?t.toUpperCase():null}}),w=h.extend(function(t){this.options={},t.forEach(function(t){this.options[t.name]=t},this)},{exists:function(t){return null!=this.options[t]},get:function(t,e){return w._get(this.options[t],e)},getAll:function(t){var e,i=this.options,n={};for(e in i)k.hasOwn(i,e)&&(n[e]=w._get(i[e],t));return n},init:function(t,e,i){"function"!=typeof i&&(i=k.noop);var n,s;for(n in this.options)k.hasOwn(this.options,n)&&(s=this.options[n],w._set(s,s.defaultValue,e),w._createAccessor(s,e,i));this._setAll(t,e,!0)},set:function(t,e,i){return this._set(t,e,i)},setAll:function(t,e){return this._setAll(t,e)},_set:function(t,e,i,n){var s=this.options[t];if(!s)throw new Error("Invalid option: "+t);if(!s.modifiable&&!n)throw new Error("Option cannot be modified: "+t);return w._set(s,e,i)},_setAll:function(t,e,i){if(!t)return!1;var n,s=!1;for(n in t)k.hasOwn(t,n)&&this._set(n,t[n],e,i)&&(s=!0);return s}},{_createAccessor:function(t,e,i){var n={get:function(){return w._get(t,e)}};t.modifiable&&(n.set=function(n){w._set(t,n,e)&&i(n,t)}),Object.defineProperty(e,t.name,n)},_get:function(t,e){return e["_"+t.name]},_set:function(t,e,i){var n="_"+t.name,s=i[n],r=t.transform(null!=e?e:t.defaultValue);return i[n]=r,r!==s}}),M=w,b=h.extend(function(){this._services={}},{getService:function(t){var e=this._services[t];if(!e)throw new Error("Service is not being managed with name: "+t);return e},setService:function(t,e){if(this._services[t])throw new Error("Service is already managed with name: "+t);e&&(this._services[t]=e)}}),B=new M([new g("background",!0,"white"),new g("backgroundAlpha",!0,1,k.abs),new g("element"),new g("foreground",!0,"black"),new g("foregroundAlpha",!0,1,k.abs),new g("level",!0,"L",k.toUpperCase),new g("mime",!0,"image/png"),new g("padding",!0,null,k.abs),new g("size",!0,100,k.abs),new g("value",!0,"")]),y=new b,O=h.extend(function(t){B.init(t,this,this.update.bind(this));var e=B.get("element",this),i=y.getService("element"),n=e&&i.isCanvas(e)?e:i.createCanvas(),s=e&&i.isImage(e)?e:i.createImage();this._canvasRenderer=new c(this,n,!0),this._imageRenderer=new m(this,s,s===e),this.update()},{get:function(){return B.getAll(this)},set:function(t){B.setAll(t,this)&&this.update()},toDataURL:function(t){return this.canvas.toDataURL(t||this.mime)},update:function(){var t=new p({level:this.level,value:this.value});this._canvasRenderer.render(t),this._imageRenderer.render(t)}},{use:function(t){y.setService(t.getName(),t)}});Object.defineProperties(O.prototype,{canvas:{get:function(){return this._canvasRenderer.getElement()}},image:{get:function(){return this._imageRenderer.getElement()}}});var A=O,L=h.extend({getName:function(){}}).extend({createCanvas:function(){},createImage:function(){},getName:function(){return"element"},isCanvas:function(t){},isImage:function(t){}}).extend({createCanvas:function(){return document.createElement("canvas")},createImage:function(){return document.createElement("img")},isCanvas:function(t){return t instanceof HTMLCanvasElement},isImage:function(t){return t instanceof HTMLImageElement}});return A.use(new L),A}); //# sourceMappingURL=qrious.min.js.map \ No newline at end of file diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index d460a4e62a..e9d22892af 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -347,7 +347,7 @@ login.login_handlers = (function() { } else { if (data.verification.method == 'OTP App'){ console.log(data.verification.totp_uri) - continue_otp(setup_completed = !data.verification.totp_uri); + continue_otp(setup_completed = data.verification.otp_setup_completed); } else if (data.verification.method == 'SMS'){ continue_sms(setup_completed=true, method_prompt=data.verification.prompt); console.log('SMS'); From cde80013692ca3d63d4d3765d2ef713bb9e2f627 Mon Sep 17 00:00:00 2001 From: B H Boma Date: Tue, 18 Jul 2017 17:30:11 +0100 Subject: [PATCH 11/67] Show twofactor auth in setup wizard --- frappe/desk/page/setup_wizard/setup_wizard.js | 30 +++++++++++++++++++ frappe/desk/page/setup_wizard/setup_wizard.py | 13 +++++++- frappe/hooks.py | 2 ++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 9b7bc1f4b6..1a0862bc91 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -563,6 +563,36 @@ var frappe_slides = [ } }, }, + { + //Two Factor Select + name:'twofactor', + domains: ["all"], + title: __("Two Factor Authentication"), + icon: "fa fa-flag", + help: __("Setup Two Factor Authentication For Users"), + fields: [ + { fieldname: "twofactor_enable", label: __("Enable Two Factor Authentication"), + fieldtype: "Check"}, + { fieldtype: "Section Break" }, + { fieldname: "twofactor_method", label: __("Select Authentication Method"), + fieldtype: "Select"} + ], + onload:function(slide){ + slide.form.fields_dict.twofactor_method.df.options = ['SMS','Email','OTP App'] + slide.form.fields_dict.twofactor_method.$wrapper.css('display','none'); + slide.get_input('twofactor_enable').change(function(){ + slide.form.fields_dict.twofactor_method.$wrapper.toggle(); + if(this.checked){ + slide.form.fields_dict.twofactor_method.df.reqd = 1; + } + else{ + slide.form.fields_dict.twofactor_method.df.reqd = 0; + } + slide.form.fields_dict.twofactor_method.refresh(); + }); + } + + } ]; var utils = { diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 8e8fef3359..d64c8d5601 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -76,8 +76,12 @@ def update_system_settings(args): 'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"), 'number_format': number_format, 'enable_scheduler': 1 if not frappe.flags.in_test else 0, - 'backup_limit': 3 # Default for downloadable backups + 'backup_limit': 3, # Default for downloadable backups + 'enable_two_factor_auth':args.get("twofactor_enable"), + 'two_factor_method':args.get('twofactor_method') }) + if args.get("twofactor_enable") == 1: + enable_twofactor_all_roles() system_settings.save() def update_user_name(args): @@ -267,3 +271,10 @@ def email_setup_wizard_exception(traceback, args): def get_language_code(lang): return frappe.db.get_value('Language', {'language_name':lang}) + + +def enable_twofactor_all_roles(): + all_role = frappe.get_doc('Role',{'role_name':'All'}) + all_role.two_factor_auth = True + all_role.save(ignore_permissions=True) + diff --git a/frappe/hooks.py b/frappe/hooks.py index 1ceab9f7ca..f9eadafca0 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -190,3 +190,5 @@ bot_parsers = [ setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception" before_write_file = "frappe.limits.validate_space_limit" + +otp_methods = ['OTP App','Email','SMS'] From 99700fb198890a2dbc5efdb47dad02ba86957bcf Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Tue, 18 Jul 2017 19:03:45 +0100 Subject: [PATCH 12/67] check if more than one role has 2fa enabled --- frappe/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/auth.py b/frappe/auth.py index 50d7f04b93..2d159b8215 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -136,7 +136,7 @@ class LoginManager: i in user_obj.roles)) two_factor_auth_user = len(frappe.db.sql(query)) - if two_factor_auth_user == 1: + if two_factor_auth_user >= 1: otp_secret = frappe.db.get_default(self.user + '_otpsecret') verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') From 51858021c8534a5adb4c6eb2df1650c221819cd6 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Wed, 19 Jul 2017 14:29:30 +0100 Subject: [PATCH 13/67] refactor code fix bug --- frappe/auth.py | 207 ++++++++++++++++++++++--------------------------- 1 file changed, 94 insertions(+), 113 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 2d159b8215..4ab689f759 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -20,7 +20,7 @@ from frappe.core.doctype.authentication_log.authentication_log import add_authen from urllib import quote -import pyotp +import pyotp,base64,os class HTTPRequest: def __init__(self): @@ -116,118 +116,104 @@ class LoginManager: self.make_session() self.set_user_info() + def two_factor_auth_user(self): + ''' Check if user has 2fa role and set otpsecret and verification method''' + two_factor_user_role = 0 + user_obj = frappe.get_doc('User', self.user) + if user_obj.roles: + query = """select name from `tabRole` where two_factor_auth=1 + and name in ("All",{0}) limit 1""".format(', '.join('\"{}\"'.format(i.role) for \ + i in user_obj.roles)) + two_factor_user_role = len(frappe.db.sql(query)) + + self.otp_secret = frappe.db.get_default(self.user + '_otpsecret') + if not self.otp_secret: + self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') + frappe.db.set_default(self.user + '_otpsecret', self.otp_secret) + frappe.db.commit() + + self.verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') + + return bool(two_factor_user_role) + + def get_verification_obj(self): + if self.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=self.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 self.verification_method == 'OTP App': + totp_uri = pyotp.TOTP(self.otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") + + if frappe.db.get_default(self.user + '_otplogin'): + otp_setup_completed = True + else: + otp_setup_completed = False + + verification_obj = {'token_delivery': True, + 'prompt': False, + 'totp_uri': totp_uri, + 'method': 'OTP App', + 'qrcode': get_qr_svg_code(totp_uri), + 'otp_setup_completed': otp_setup_completed} + elif self.verification_method == 'Email': + status = self.send_token_via_email(token=token,otpsecret=self.otp_secret) + verification_obj = {'token_delivery': status, + 'prompt': status and 'Enter verification code sent to your registered email address', + 'method': 'Email'} + return verification_obj + + def process_2fa(self): + if self.two_factor_auth_user(): + token = int(pyotp.TOTP(self.otp_secret).now()) + verification_obj = self.get_verification_obj() + + tmp_id = frappe.generate_hash(length=8) + usr = frappe.form_dict.get('usr') + pwd = frappe.form_dict.get('pwd') + + if self.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', self.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,180) + + frappe.local.response['verification'] = verification_obj + frappe.local.response['tmp_id'] = tmp_id + + raise frappe.RequestToken + else: + self.post_login(no_two_auth=True) + def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) - if frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') == unicode(0): - self.authenticate() - self.post_login(no_two_auth=True) + otp = frappe.form_dict.get('otp') + if otp: + try: + 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']) + except: + pass + self.post_login() else: - otp = frappe.form_dict.get('otp') - if not otp: - self.authenticate() - # after authenticate, self.user is set (from check_password() call) - user_obj = frappe.get_doc('User', self.user) - two_factor_auth_user = 0 - if user_obj.roles: - query = """select name from `tabRole` where two_factor_auth=1 - and name in ("All"{0}) limit 1""".format(', '.join('\"{}\"'.format(i.role) for \ - i in user_obj.roles)) - two_factor_auth_user = len(frappe.db.sql(query)) - - if two_factor_auth_user >= 1: - - otp_secret = frappe.db.get_default(self.user + '_otpsecret') - verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') - - 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 = pyotp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") - - if frappe.db.get_default(self.user + '_otplogin'): - otp_setup_completed = True - else: - otp_setup_completed = False - - verification_obj = {'token_delivery': True, - 'prompt': False, - 'totp_uri': totp_uri, - 'method': 'OTP App', - 'qrcode': get_qr_svg_code(totp_uri), - 'otp_setup_completed': otp_setup_completed} - 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: - import os - import base64 - otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') - totp_uri = pyotp.totp.TOTP(otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") - - # 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, - 'method': verification_method, - 'token_delivery': True, - 'prompt': False, - 'totp_uri': totp_uri, - 'qrcode':get_qr_svg_code(totp_uri), - 'otp_setup_completed': False, - #'restrict_method': int(restrict_method) and (fixed_method[0].default_method or 'OTP App') - } - - tmp_id = frappe.generate_hash(length=8) - usr = frappe.form_dict.get('usr') - pwd = frappe.form_dict.get('pwd') - - 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,180) - - frappe.local.response['tmp_id'] = tmp_id - - raise frappe.RequestToken - - else: - self.post_login(no_two_auth=True) - + self.authenticate() + if frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') == unicode(1): + self.process_2fa() else: - try: - 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']) - except: - pass - # frappe.log_error(frappe.get_traceback(),"AUTHENTICATION PROBLEM") - self.post_login() + self.post_login(no_two_auth=True) def post_login(self,no_two_auth=False): self.run_trigger('on_login') @@ -242,7 +228,7 @@ class LoginManager: self.make_session() self.set_user_info() - def confirm_token(self,otp=None, tmp_id=None, hotp_token=False): + def confirm_token(self, otp=None, tmp_id=None, hotp_token=False): try: otp_secret = frappe.cache().get(tmp_id + '_otp_secret') if not otp_secret: @@ -253,9 +239,6 @@ class LoginManager: 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: @@ -264,8 +247,6 @@ class LoginManager: totp = pyotp.TOTP(otp_secret) if totp.verify(otp): # show qr code only once - if not frappe.db.get_default(self.user + '_otpsecret'): - frappe.db.set_default(self.user + '_otpsecret', otp_secret) if not frappe.db.get_default(self.user + '_otplogin'): frappe.db.set_default(self.user + '_otplogin', 1) return True From aa45ffcac6d2a61c3b03236c0513848c3f6d682b Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Fri, 21 Jul 2017 14:32:04 +0100 Subject: [PATCH 14/67] fix website user login bug refactor JS --- frappe/auth.py | 36 +-- frappe/core/doctype/user/user.py | 33 ++- frappe/hooks.py | 3 +- frappe/public/js/frappe/qrious.min.js | 6 - frappe/templates/includes/login/login.js | 305 ++++++++--------------- 5 files changed, 144 insertions(+), 239 deletions(-) delete mode 100644 frappe/public/js/frappe/qrious.min.js diff --git a/frappe/auth.py b/frappe/auth.py index 4ab689f759..003637da5c 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -124,7 +124,7 @@ class LoginManager: query = """select name from `tabRole` where two_factor_auth=1 and name in ("All",{0}) limit 1""".format(', '.join('\"{}\"'.format(i.role) for \ i in user_obj.roles)) - two_factor_user_role = len(frappe.db.sql(query)) + two_factor_user_role = len(frappe.db.sql(query)) self.otp_secret = frappe.db.get_default(self.user + '_otpsecret') if not self.otp_secret: @@ -152,12 +152,10 @@ class LoginManager: else: otp_setup_completed = False - verification_obj = {'token_delivery': True, - 'prompt': False, - 'totp_uri': totp_uri, + verification_obj = {'totp_uri': totp_uri, 'method': 'OTP App', 'qrcode': get_qr_svg_code(totp_uri), - 'otp_setup_completed': otp_setup_completed} + 'setup': otp_setup_completed } elif self.verification_method == 'Email': status = self.send_token_via_email(token=token,otpsecret=self.otp_secret) verification_obj = {'token_delivery': status, @@ -174,9 +172,13 @@ class LoginManager: usr = frappe.form_dict.get('usr') pwd = frappe.form_dict.get('pwd') + # set increased expiry time for SMS and Email if self.verification_method in ['SMS', 'Email']: - frappe.cache().set(tmp_id + '_token',token) - frappe.cache().expire(tmp_id + '_token',300) + expiry_time = 300 + frappe.cache().set(tmp_id + '_token', token) + frappe.cache().expire(tmp_id + '_token', expiry_time) + else: + expiry_time = 180 frappe.cache().set(tmp_id + '_usr', usr) frappe.cache().set(tmp_id + '_pwd', pwd) @@ -184,7 +186,7 @@ class LoginManager: 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,180) + frappe.cache().expire(field, expiry_time) frappe.local.response['verification'] = verification_obj frappe.local.response['tmp_id'] = tmp_id @@ -232,7 +234,7 @@ class LoginManager: try: otp_secret = frappe.cache().get(tmp_id + '_otp_secret') if not otp_secret: - frappe.throw('Login session expired, please refresh page to try again') + frappe.throw('Login session expired. Refresh page to try again') except AttributeError: return False @@ -395,21 +397,24 @@ class LoginManager: def clear_cookies(self): clear_cookies() - def send_token_via_sms(self,otpsecret,token=None,phone_no=None): + def send_token_via_sms(self, otpsecret, token=None, phone_no=None): try: from erpnext.setup.doctype.sms_settings.sms_settings import send_request except: return False + + if not phone_no: + return False + 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) @@ -419,13 +424,14 @@ class LoginManager: else: return False - def send_token_via_email(self,token,otpsecret): - user_email = frappe.db.get_value('User',self.user, 'email') + 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='

Your verification code is {}

'.format(hotp.at(int(token))),delayed=False, retry=3) + message='

Your verification code is {}

'.format(hotp.at(int(token))), + delayed=False, retry=3) return True class CookieManager: diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index d2343672fb..7f05f5def8 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -928,17 +928,18 @@ def update_gravatar(name): @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 + try: + from erpnext.setup.doctype.sms_settings.sms_settings import send_request + except: + return False 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 + token = frappe.cache().get(tmp_id + '_token') args = {ss.message_parameter: 'verification code is {}'.format(token)} for d in ss.get("parameters"): @@ -956,7 +957,6 @@ def send_token_via_sms(tmp_id,phone_no=None,user=None): return False args[ss.receiver_parameter] = usr_phone - status = send_request(ss.sms_gateway_url, args) if 200 <= status < 300: @@ -971,22 +971,19 @@ def send_token_via_email(tmp_id,token=None): 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='

Your verification code is {0}

'.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() \ No newline at end of file + otpsecret = frappe.cache().get(tmp_id + '_otp_secret') + hotp = pyotp.HOTP(otpsecret) + + frappe.sendmail( + recipients=user_email, sender=None, subject='Verification Code', + message='

Your verification code is {0}

'.format(hotp.at(int(count))), + delayed=False, retry=3) + + return True diff --git a/frappe/hooks.py b/frappe/hooks.py index f9eadafca0..49ec772175 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -40,8 +40,7 @@ app_include_css = [ ] web_include_js = [ - "website_script.js", - "assets/js/qrious.min.js" + "website_script.js" ] bootstrap = "assets/frappe/css/bootstrap.css" diff --git a/frappe/public/js/frappe/qrious.min.js b/frappe/public/js/frappe/qrious.min.js deleted file mode 100644 index 5943f8c0f9..0000000000 --- a/frappe/public/js/frappe/qrious.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! QRious v4.0.2 | (C) 2017 Alasdair Mercer | GPL v3 License -Based on jsqrencode | (C) 2010 tz@execpc.com | GPL v3 License -*/ -!function(t,e){console.log(t);console.log(e);"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.QRious=e()}(this,function(){"use strict";function t(t,e){var n;return"function"==typeof Object.create?n=Object.create(t):(s.prototype=t,n=new s,s.prototype=null),e&&i(!0,n,e),n}function e(e,n,s,r){var o=this;return"string"!=typeof e&&(r=s,s=n,n=e,e=null),"function"!=typeof n&&(r=s,s=n,n=function(){return o.apply(this,arguments)}),i(!1,n,o,r),n.prototype=t(o.prototype,s),n.prototype.constructor=n,n.class_=e||o.class_,n.super_=o,n}function i(t,e,i){for(var n,s,a=0,h=(i=o.call(arguments,2)).length;a>1&1,n=0;n0;e--)n[e]=n[e]?n[e-1]^_.EXPONENT[v._modN(_.LOG[n[e]]+t)]:n[e-1];n[0]=_.EXPONENT[v._modN(_.LOG[n[0]]+t)]}for(t=0;t<=i;t++)n[t]=_.LOG[n[t]]},_checkBadness:function(){var t,e,i,n,s,r=0,o=this._badness,a=this.buffer,h=this.width;for(s=0;sh*h;)u-=h*h,c++;for(r+=c*v.N4,n=0;n=o-2&&(t=o-2,s>9&&t--);var a=t;if(s>9){for(r[a+2]=0,r[a+3]=0;a--;)e=r[a],r[a+3]|=255&e<<4,r[a+2]=e>>4;r[2]|=255&t<<4,r[1]=t>>4,r[0]=64|t>>12}else{for(r[a+1]=0,r[a+2]=0;a--;)e=r[a],r[a+2]|=255&e<<4,r[a+1]=e>>4;r[1]|=255&t<<4,r[0]=64|t>>4}for(a=t+3-(s<10);a=5&&(i+=v.N1+n[e]-5);for(e=3;et||3*n[e-3]>=4*n[e]||3*n[e+3]>=4*n[e])&&(i+=v.N3);return i},_finish:function(){this._stringBuffer=this.buffer.slice();var t,e,i=0,n=3e4;for(e=0;e<8&&(this._applyMask(e),(t=this._checkBadness())>=1)1&n&&(s[r-1-e+8*r]=1,e<6?s[8+r*e]=1:s[8+r*(e+1)]=1);for(e=0;e<7;e++,n>>=1)1&n&&(s[8+r*(r-7+e)]=1,e?s[6-e+8*r]=1:s[7+8*r]=1)},_interleaveBlocks:function(){var t,e,i=this._dataBlock,n=this._ecc,s=this._eccBlock,r=0,o=this._calculateMaxLength(),a=this._neccBlock1,h=this._neccBlock2,f=this._stringBuffer;for(t=0;t1)for(t=u.BLOCK[n],i=s-7;;){for(e=s-7;e>t-3&&(this._addAlignment(e,i),!(e6)for(t=d.BLOCK[r-7],e=17,i=0;i<6;i++)for(n=0;n<3;n++,e--)1&(e>11?r>>e-12:t>>e)?(s[5-i+o*(2-n+o-11)]=1,s[2-n+o-11+o*(5-i)]=1):(this._setMask(5-i,2-n+o-11),this._setMask(2-n+o-11,5-i))},_isMasked:function(t,e){var i=v._getMaskBit(t,e);return 1===this._mask[i]},_pack:function(){var t,e,i,n=1,s=1,r=this.width,o=r-1,a=r-1,h=(this._dataBlock+this._eccBlock)*(this._neccBlock1+this._neccBlock2)+this._neccBlock2;for(e=0;ee&&(i=t,t=e,e=i),i=e,i+=e*e,i>>=1,i+=t},_modN:function(t){for(;t>=255;)t=((t-=255)>>8)+(255&t);return t},N1:3,N2:3,N3:40,N4:10}),p=v,m=f.extend({draw:function(){this.element.src=this.qrious.toDataURL()},reset:function(){this.element.src=""},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),g=h.extend(function(t,e,i,n){this.name=t,this.modifiable=Boolean(e),this.defaultValue=i,this._valueTransformer=n},{transform:function(t){var e=this._valueTransformer;return"function"==typeof e?e(t,this):t}}),k=h.extend(null,{abs:function(t){return null!=t?Math.abs(t):null},hasOwn:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},noop:function(){},toUpperCase:function(t){return null!=t?t.toUpperCase():null}}),w=h.extend(function(t){this.options={},t.forEach(function(t){this.options[t.name]=t},this)},{exists:function(t){return null!=this.options[t]},get:function(t,e){return w._get(this.options[t],e)},getAll:function(t){var e,i=this.options,n={};for(e in i)k.hasOwn(i,e)&&(n[e]=w._get(i[e],t));return n},init:function(t,e,i){"function"!=typeof i&&(i=k.noop);var n,s;for(n in this.options)k.hasOwn(this.options,n)&&(s=this.options[n],w._set(s,s.defaultValue,e),w._createAccessor(s,e,i));this._setAll(t,e,!0)},set:function(t,e,i){return this._set(t,e,i)},setAll:function(t,e){return this._setAll(t,e)},_set:function(t,e,i,n){var s=this.options[t];if(!s)throw new Error("Invalid option: "+t);if(!s.modifiable&&!n)throw new Error("Option cannot be modified: "+t);return w._set(s,e,i)},_setAll:function(t,e,i){if(!t)return!1;var n,s=!1;for(n in t)k.hasOwn(t,n)&&this._set(n,t[n],e,i)&&(s=!0);return s}},{_createAccessor:function(t,e,i){var n={get:function(){return w._get(t,e)}};t.modifiable&&(n.set=function(n){w._set(t,n,e)&&i(n,t)}),Object.defineProperty(e,t.name,n)},_get:function(t,e){return e["_"+t.name]},_set:function(t,e,i){var n="_"+t.name,s=i[n],r=t.transform(null!=e?e:t.defaultValue);return i[n]=r,r!==s}}),M=w,b=h.extend(function(){this._services={}},{getService:function(t){var e=this._services[t];if(!e)throw new Error("Service is not being managed with name: "+t);return e},setService:function(t,e){if(this._services[t])throw new Error("Service is already managed with name: "+t);e&&(this._services[t]=e)}}),B=new M([new g("background",!0,"white"),new g("backgroundAlpha",!0,1,k.abs),new g("element"),new g("foreground",!0,"black"),new g("foregroundAlpha",!0,1,k.abs),new g("level",!0,"L",k.toUpperCase),new g("mime",!0,"image/png"),new g("padding",!0,null,k.abs),new g("size",!0,100,k.abs),new g("value",!0,"")]),y=new b,O=h.extend(function(t){B.init(t,this,this.update.bind(this));var e=B.get("element",this),i=y.getService("element"),n=e&&i.isCanvas(e)?e:i.createCanvas(),s=e&&i.isImage(e)?e:i.createImage();this._canvasRenderer=new c(this,n,!0),this._imageRenderer=new m(this,s,s===e),this.update()},{get:function(){return B.getAll(this)},set:function(t){B.setAll(t,this)&&this.update()},toDataURL:function(t){return this.canvas.toDataURL(t||this.mime)},update:function(){var t=new p({level:this.level,value:this.value});this._canvasRenderer.render(t),this._imageRenderer.render(t)}},{use:function(t){y.setService(t.getName(),t)}});Object.defineProperties(O.prototype,{canvas:{get:function(){return this._canvasRenderer.getElement()}},image:{get:function(){return this._imageRenderer.getElement()}}});var A=O,L=h.extend({getName:function(){}}).extend({createCanvas:function(){},createImage:function(){},getName:function(){return"element"},isCanvas:function(t){},isImage:function(t){}}).extend({createCanvas:function(){return document.createElement("canvas")},createImage:function(){return document.createElement("img")},isCanvas:function(t){return t instanceof HTMLCanvasElement},isImage:function(t){return t instanceof HTMLImageElement}});return A.use(new L),A}); - -//# sourceMappingURL=qrious.min.js.map \ No newline at end of file diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index e9d22892af..8d3c7f63f3 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -160,204 +160,16 @@ login.login_handlers = (function() { if(data.verification) { login.set_indicator("{{ _("Success") }}", 'green'); - var continue_otp = function(setup_completed,method_prompt){ - - $('.login-content').empty().append($('
').attr({'id':'otp_div'}).html( - '
\ -
\ - Verification\ -
\ - \ - \ -
')); - - verify_token(); - - if (!setup_completed){ - var qrcode = $('
') - qrcode.attr('id','qrcode_div'); - qrcode.css('text-align','center'); - - var direction = $('
').attr('id','qr_info').text(method_prompt || 'Scan QR Code and enter the resulting code displayed'); - - var qrimg = $(''); - qrimg.attr('src','data:image/svg+xml;base64,' + data.verification.qrcode); - qrcode.append(direction); - qrcode.append(qrimg); - $('#otp_div').prepend(qrcode); - } else { - var qrcode = $('
').attr('id','qrcode_div'); - var direction = $('
').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($('
').attr({'id':'otp_div'}).html( - '
\ -
\ - Verification\ -
\ - \ - \ -
')); - - verify_token(); - - if (!setup_completed){ - var sms_div = $('
').attr({'id':'sms_div','style':'margin-bottom: 20px;'}); - var direction = $('
').attr({'id':'sms_info','style':'margin-bottom: 15px;'}).text('Enter phone number to send verification code'); - sms_div.append(direction); - sms_div.append($('
').attr({'id':'sms_code_div'}).html( - '
\ - \ - \ -

')); - - $('#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( - '

SMS sent.
Enter verification code received


' - ); - } else { - $('#sms_div').empty().append( - '

SMS not sent


' - ); - } - } - }); - }) - } else { - var smscode = $('
').attr('id','smscode_div'); - var direction = $('
').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($('
').attr({'id':'otp_div'}).html( - '
\ -
\ - Verification\ -
\ - \ - \ -
')); - - verify_token(); - - if (!setup_completed){ - var email_div = $('
').attr({'id':'email_div','style':'margin-bottom: 20px;'}); - email_div.append('

Verification code email will be sent to registered email address. Enter code received below

') - - $('#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( - '

Email not sent


' - ); - } - } - }); - } else { - if (method_prompt){ - var emailcode = $('
').attr('id','emailcode_div'); - var direction = $('
').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 = $('
').attr('id','emailcode_div'); - var direction = $('
').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('
\ - //
\ - //

Select verification Method
\ - // method may be changed later in settings

\ - //
\ - //
\ - // \ - //
\ - //
\ - // \ - //
\ - //
\ - // \ - //
\ - // \ - //
') - - // 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 (data.verification.method == 'OTP App'){ - continue_otp(setup_completed=false); - } else if (data.verification.method == 'SMS'){ - continue_sms(setup_completed=false); - } else if (data.verification.method == 'Email'){ - continue_email(setup_completed=false); - } + document.cookie = "tmp_id="+data.tmp_id; - // 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 { - if (data.verification.method == 'OTP App'){ - console.log(data.verification.totp_uri) - continue_otp(setup_completed = data.verification.otp_setup_completed); - } 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); - } + if (data.verification.method == 'OTP App'){ + continue_otp_app(data.verification.setup, data.verification.qrcode); + } else if (data.verification.method == 'SMS'){ + continue_sms(data.verification.setup, data.verification.prompt); + } else if (data.verification.method == 'Email'){ + continue_sms(data.verification.setup, data.verification.prompt); } - document.cookie = "tmp_id="+data.tmp_id; - //verify_token(); return false; } else if(data.message == 'Logged In'){ @@ -410,10 +222,7 @@ login.login_handlers = (function() { frappe.ready(function() { - login.bind_events(); - console.log("Why"); - if (!window.location.hash) { window.location.hash = "#login"; @@ -440,3 +249,103 @@ var verify_token = function(event) { return false; }); } + +var request_otp = function(r){ + $('.login-content').empty().append($('
').attr({'id':'twofactor_div'}).html( + '
\ +
\ + Verification\ +
\ +
\ + \ + \ +
')); + // add event handler for submit button + verify_token(); +} + +var continue_otp_app = function(setup, qrcode){ + request_otp(); + var qrcode_div = $('
').attr({'id':'qrcode_div','style':'text-align:center;padding-bottom:15px;'}); + + if (!setup){ + direction = $('
').attr('id','qr_info').text('Scan QR Code and enter the resulting code displayed' ), + qrimg = $('').attr({ + 'src':'data:image/svg+xml;base64,' + qrcode, + 'style':'width:250px;height:250px;'}); + + qrcode_div.append(direction); + qrcode_div.append(qrimg); + $('#otp_div').prepend(qrcode_div); + } else { + direction = $('
').attr('id','qr_info').text('Enter Code displayed in OTP App'); + qrcode_div.append(direction); + $('#otp_div').prepend(qrcode_div); + } +} + +var continue_sms = function(setup, prompt){ + request_otp(); + var sms_div = $('
').attr({'id':'sms_div','style':'padding-bottom:15px;text-align:center;'}); + + if (!setup){ + direction = $('
').attr('id','sms_info').text('Enter phone number to send verification code'); + sms_div.append(direction); + sms_div.append($('
').attr({'id':'sms_code_div'}).html( + '
\ + \ + \ +

')); + + $('#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( + '

SMS sent.
Enter verification code received


' + ); + } else { + $('#sms_div').empty().append( + '

SMS not sent


' + ); + } + } + }); + }) + } else { + direction = $('
').attr('id','qr_info').text(prompt || 'SMS not sent'); + sms_div.append(direction); + $('#otp_div').prepend(sms_div) + } +} + +var continue_email = function(setup, prompt){ + request_otp(); + var email_div = $('
').attr({'id':'email_div','style':'padding-bottom:15px;text-align:center;'}); + + if (!setup){ + email_div.append('

Verification code email will be sent to registered email address. Enter code received below

') + $('#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( + '

Email not sent


' + ); + } + } + }); + } else { + var direction = $('
').attr('id','qr_info').text(prompt || 'Verification code email not sent'); + email_div.append(direction); + $('#otp_div').prepend(email_div); + } +} \ No newline at end of file From 6f4e39fd4697c68efa25d086069ca6e0e4df8433 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Fri, 21 Jul 2017 17:50:31 +0100 Subject: [PATCH 15/67] fix Email otp method queue email sending --- frappe/auth.py | 29 ++++++++++++------------ frappe/templates/includes/login/login.js | 6 ++--- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 003637da5c..90c4a4cf49 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -140,7 +140,7 @@ class LoginManager: if self.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=self.otp_secret) + status = self.send_token_via_sms(token=self.token, phone_no=usr_phone, otpsecret=self.otp_secret) verification_obj = {'token_delivery': status, 'prompt': status and 'Enter verification code sent to {}'.format(usr_phone[:4] + '******' + usr_phone[-3:]), 'method': 'SMS'} @@ -157,7 +157,7 @@ class LoginManager: 'qrcode': get_qr_svg_code(totp_uri), 'setup': otp_setup_completed } elif self.verification_method == 'Email': - status = self.send_token_via_email(token=token,otpsecret=self.otp_secret) + status = self.send_token_via_email(token=self.token, otpsecret=self.otp_secret) verification_obj = {'token_delivery': status, 'prompt': status and 'Enter verification code sent to your registered email address', 'method': 'Email'} @@ -165,7 +165,7 @@ class LoginManager: def process_2fa(self): if self.two_factor_auth_user(): - token = int(pyotp.TOTP(self.otp_secret).now()) + self.token = int(pyotp.TOTP(self.otp_secret).now()) verification_obj = self.get_verification_obj() tmp_id = frappe.generate_hash(length=8) @@ -175,7 +175,7 @@ class LoginManager: # set increased expiry time for SMS and Email if self.verification_method in ['SMS', 'Email']: expiry_time = 300 - frappe.cache().set(tmp_id + '_token', token) + frappe.cache().set(tmp_id + '_token', self.token) frappe.cache().expire(tmp_id + '_token', expiry_time) else: expiry_time = 180 @@ -212,7 +212,7 @@ class LoginManager: self.post_login() else: self.authenticate() - if frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') == unicode(1): + if (self.user != 'Administrator') and (frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') == unicode(1)): self.process_2fa() else: self.post_login(no_two_auth=True) @@ -417,21 +417,22 @@ class LoginManager: args[ss.receiver_parameter] = phone_no - status = send_request(ss.sms_gateway_url, args) - - if 200 <= status < 300: - return True - else: - return False + sms_args = {'gateway_url':ss.sms_gateway_url,'params':args} + enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args) + return True def send_token_via_email(self, token, otpsecret): + from frappe.utils.background_jobs import enqueue 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='

Your verification code is {}

'.format(hotp.at(int(token))), - delayed=False, retry=3) + email_args = { + 'recipients':user_email, 'sender':None, 'subject':'Verification Code', + 'message':'

Your verification code is {}

'.format(hotp.at(int(token))), + 'delayed':False, 'retry':3 } + + enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) return True class CookieManager: diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 8d3c7f63f3..91477069ba 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -167,7 +167,7 @@ login.login_handlers = (function() { } else if (data.verification.method == 'SMS'){ continue_sms(data.verification.setup, data.verification.prompt); } else if (data.verification.method == 'Email'){ - continue_sms(data.verification.setup, data.verification.prompt); + continue_email(data.verification.setup, data.verification.prompt); } return false; @@ -288,7 +288,7 @@ var continue_sms = function(setup, prompt){ request_otp(); var sms_div = $('
').attr({'id':'sms_div','style':'padding-bottom:15px;text-align:center;'}); - if (!setup){ + if (setup){ direction = $('
').attr('id','sms_info').text('Enter phone number to send verification code'); sms_div.append(direction); sms_div.append($('
').attr({'id':'sms_code_div'}).html( @@ -328,7 +328,7 @@ var continue_email = function(setup, prompt){ request_otp(); var email_div = $('
').attr({'id':'email_div','style':'padding-bottom:15px;text-align:center;'}); - if (!setup){ + if (setup){ email_div.append('

Verification code email will be sent to registered email address. Enter code received below

') $('#otp_div').prepend(email_div); frappe.call({ From b6e65030d8341f9abecdf2bee81dff1a66a6077e Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Sat, 22 Jul 2017 23:29:00 +0100 Subject: [PATCH 16/67] Transfer of SMS Settings and SMS Parameter to Frappe from ERPNext. Triggered by the need for SMS Sending by the Two-Factor Authentication functionality contributed by Manqala --- frappe/core/doctype/sms_parameter/README.md | 1 + frappe/core/doctype/sms_parameter/__init__.py | 1 + .../doctype/sms_parameter/sms_parameter.json | 98 +++++++ .../doctype/sms_parameter/sms_parameter.py | 10 + frappe/core/doctype/sms_settings/README.md | 1 + frappe/core/doctype/sms_settings/__init__.py | 1 + .../core/doctype/sms_settings/sms_settings.js | 0 .../doctype/sms_settings/sms_settings.json | 267 ++++++++++++++++++ .../core/doctype/sms_settings/sms_settings.py | 117 ++++++++ .../doctype/sms_settings/test_sms_settings.js | 23 ++ frappe/core/doctype/user/user.py | 2 +- 11 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 frappe/core/doctype/sms_parameter/README.md create mode 100755 frappe/core/doctype/sms_parameter/__init__.py create mode 100755 frappe/core/doctype/sms_parameter/sms_parameter.json create mode 100644 frappe/core/doctype/sms_parameter/sms_parameter.py create mode 100644 frappe/core/doctype/sms_settings/README.md create mode 100755 frappe/core/doctype/sms_settings/__init__.py create mode 100644 frappe/core/doctype/sms_settings/sms_settings.js create mode 100755 frappe/core/doctype/sms_settings/sms_settings.json create mode 100644 frappe/core/doctype/sms_settings/sms_settings.py create mode 100644 frappe/core/doctype/sms_settings/test_sms_settings.js diff --git a/frappe/core/doctype/sms_parameter/README.md b/frappe/core/doctype/sms_parameter/README.md new file mode 100644 index 0000000000..5935a390d2 --- /dev/null +++ b/frappe/core/doctype/sms_parameter/README.md @@ -0,0 +1 @@ +SMS query parameter for SMS Settings. \ No newline at end of file diff --git a/frappe/core/doctype/sms_parameter/__init__.py b/frappe/core/doctype/sms_parameter/__init__.py new file mode 100755 index 0000000000..baffc48825 --- /dev/null +++ b/frappe/core/doctype/sms_parameter/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.json b/frappe/core/doctype/sms_parameter/sms_parameter.json new file mode 100755 index 0000000000..b5648ade80 --- /dev/null +++ b/frappe/core/doctype/sms_parameter/sms_parameter.json @@ -0,0 +1,98 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2013-02-22 01:27:58", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "editable_grid": 1, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "parameter", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Parameter", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "150px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "150px" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": "150px", + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0, + "width": "150px" + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-07-22 22:52:53.309396", + "modified_by": "chude.osiegbu@manqala.com", + "module": "Core", + "name": "SMS Parameter", + "owner": "Administrator", + "permissions": [], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.py b/frappe/core/doctype/sms_parameter/sms_parameter.py new file mode 100644 index 0000000000..08b220b61a --- /dev/null +++ b/frappe/core/doctype/sms_parameter/sms_parameter.py @@ -0,0 +1,10 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +from frappe.model.document import Document + +class SMSParameter(Document): + pass \ No newline at end of file diff --git a/frappe/core/doctype/sms_settings/README.md b/frappe/core/doctype/sms_settings/README.md new file mode 100644 index 0000000000..4fb49803b3 --- /dev/null +++ b/frappe/core/doctype/sms_settings/README.md @@ -0,0 +1 @@ +Settings for automatically sending SMS from the system. \ No newline at end of file diff --git a/frappe/core/doctype/sms_settings/__init__.py b/frappe/core/doctype/sms_settings/__init__.py new file mode 100755 index 0000000000..baffc48825 --- /dev/null +++ b/frappe/core/doctype/sms_settings/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/frappe/core/doctype/sms_settings/sms_settings.js b/frappe/core/doctype/sms_settings/sms_settings.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json new file mode 100755 index 0000000000..0898ed389e --- /dev/null +++ b/frappe/core/doctype/sms_settings/sms_settings.json @@ -0,0 +1,267 @@ +{ + "allow_copy": 1, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2013-01-10 16:34:24", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "editable_grid": 0, + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "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, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, + "width": "50%" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Eg. smsgateway.com/api/send_sms.cgi", + "fieldname": "sms_gateway_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "SMS Gateway URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Enter url parameter for message", + "fieldname": "message_parameter", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Message Parameter", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Enter url parameter for receiver nos", + "fieldname": "receiver_parameter", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Receiver Parameter", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sms_sender_name", + "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": "SMS Sender Name", + "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, + "fieldname": "static_parameters_section", + "fieldtype": "Column Break", + "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, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, + "width": "50%" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)", + "fieldname": "parameters", + "fieldtype": "Table", + "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": "Static Parameters", + "length": 0, + "no_copy": 0, + "options": "SMS Parameter", + "permlevel": 0, + "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, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-cog", + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-22 22:52:16.066981", + "modified_by": "chude.osiegbu@manqala.com", + "module": "Core", + "name": "SMS Settings", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py new file mode 100644 index 0000000000..a8b59beffa --- /dev/null +++ b/frappe/core/doctype/sms_settings/sms_settings.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +from frappe import _, throw, msgprint +from frappe.utils import nowdate + +from frappe.model.document import Document + +class SMSSettings(Document): + pass + +def validate_receiver_nos(receiver_list): + validated_receiver_list = [] + for d in receiver_list: + # remove invalid character + for x in [' ', '+', '-', '(', ')']: + d = d.replace(x, '') + + validated_receiver_list.append(d) + + if not validated_receiver_list: + throw(_("Please enter valid mobile nos")) + + return validated_receiver_list + + +def get_sender_name(): + "returns name as SMS sender" + sender_name = frappe.db.get_single_value('SMS Settings', 'sms_sender_name') or \ + 'ERPNXT' + if len(sender_name) > 6 and \ + frappe.db.get_default("country") == "India": + throw("""As per TRAI rule, sender name must be exactly 6 characters. + Kindly change sender name in Setup --> Global Defaults. + Note: Hyphen, space, numeric digit, special characters are not allowed.""") + return sender_name + +@frappe.whitelist() +def get_contact_number(contact_name, ref_doctype, ref_name): + "returns mobile number of the contact" + number = frappe.db.sql("""select mobile_no, phone from tabContact + where name=%s + and exists( + select name from `tabDynamic Link` where link_doctype=%s and link_name=%s + ) + """, (contact_name, ref_doctype, ref_name)) + + return number and (number[0][0] or number[0][1]) or '' + +@frappe.whitelist() +def send_sms(receiver_list, msg, sender_name = '', success_msg = True): + + import json + if isinstance(receiver_list, basestring): + receiver_list = json.loads(receiver_list) + if not isinstance(receiver_list, list): + receiver_list = [receiver_list] + + receiver_list = validate_receiver_nos(receiver_list) + + arg = { + 'receiver_list' : receiver_list, + 'message' : unicode(msg).encode('utf-8'), + 'sender_name' : sender_name or get_sender_name(), + 'success_msg' : success_msg + } + + if frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'): + send_via_gateway(arg) + else: + msgprint(_("Please Update SMS Settings")) + +def send_via_gateway(arg): + ss = frappe.get_doc('SMS Settings', 'SMS Settings') + args = {ss.message_parameter: arg.get('message')} + for d in ss.get("parameters"): + args[d.parameter] = d.value + + success_list = [] + for d in arg.get('receiver_list'): + args[ss.receiver_parameter] = d + status = send_request(ss.sms_gateway_url, args) + + if 200 <= status < 300: + success_list.append(d) + + if len(success_list) > 0: + args.update(arg) + create_sms_log(args, success_list) + if arg.get('success_msg'): + frappe.msgprint(_("SMS sent to following numbers: {0}").format("\n" + "\n".join(success_list))) + + +def send_request(gateway_url, params): + import requests + response = requests.get(gateway_url, params = params, headers={'Accept': "text/plain, text/html, */*"}) + response.raise_for_status() + return response.status_code + + +# Create SMS Log +# ========================================================= +def create_sms_log(args, sent_to): + sl = frappe.new_doc('SMS Log') + sl.sender_name = args['sender_name'] + sl.sent_on = nowdate() + sl.message = args['message'].decode('utf-8') + sl.no_of_requested_sms = len(args['receiver_list']) + sl.requested_numbers = "\n".join(args['receiver_list']) + sl.no_of_sent_sms = len(sent_to) + sl.sent_to = "\n".join(sent_to) + sl.flags.ignore_permissions = True + sl.save() diff --git a/frappe/core/doctype/sms_settings/test_sms_settings.js b/frappe/core/doctype/sms_settings/test_sms_settings.js new file mode 100644 index 0000000000..c090d167f5 --- /dev/null +++ b/frappe/core/doctype/sms_settings/test_sms_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: SMS Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('SMS Settings', [ + // insert a new SMS Settings + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 57ef728654..0bcfe0ec2b 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -929,7 +929,7 @@ def update_gravatar(name): @frappe.whitelist(allow_guest=True) def send_token_via_sms(tmp_id,phone_no=None,user=None): try: - from erpnext.setup.doctype.sms_settings.sms_settings import send_request + from frappe.core.doctype.sms_settings.sms_settings import send_request except: return False From 9b4f10c204a1936b7b495ece19a7ae529b734225 Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Sun, 23 Jul 2017 00:27:01 +0100 Subject: [PATCH 17/67] Fixed issue with SMS sending --- frappe/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 11e054d1c4..cee4752edd 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -16,6 +16,7 @@ from frappe.modules.patch_handler import check_session_stopped from frappe.translate import get_lang_code from frappe.utils.password import check_password from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log +from frappe.utils.background_jobs import enqueue from urllib import quote @@ -399,7 +400,7 @@ class LoginManager: def send_token_via_sms(self, otpsecret, token=None, phone_no=None): try: - from erpnext.setup.doctype.sms_settings.sms_settings import send_request + from frappe.core.doctype.sms_settings.sms_settings import send_request except: return False @@ -411,7 +412,7 @@ class LoginManager: return False hotp = pyotp.HOTP(otpsecret) - args = {ss.message_parameter: 'verification code is {}'.format(hotp.at(int(token)))} + args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token)))} for d in ss.get("parameters"): args[d.parameter] = d.value @@ -422,7 +423,6 @@ class LoginManager: return True def send_token_via_email(self, token, otpsecret): - from frappe.utils.background_jobs import enqueue user_email = frappe.db.get_value('User', self.user, 'email') if not user_email: return False From 97c6d747890931ff8072ec86f9b0c286017aeb77 Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Sun, 23 Jul 2017 02:16:12 +0100 Subject: [PATCH 18/67] Updates to System Settings and login.js to allow for specification of the name of the token issuer. --- frappe/auth.py | 11 +++--- .../system_settings/system_settings.json | 34 ++++++++++++++++++- .../system_settings/test_system_settings.js | 23 +++++++++++++ frappe/templates/includes/login/login.js | 3 +- 4 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 frappe/core/doctype/system_settings/test_system_settings.js diff --git a/frappe/auth.py b/frappe/auth.py index cee4752edd..f6823d7b2d 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -138,6 +138,7 @@ class LoginManager: return bool(two_factor_user_role) def get_verification_obj(self): + otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') if self.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 @@ -146,7 +147,7 @@ class LoginManager: 'prompt': status and 'Enter verification code sent to {}'.format(usr_phone[:4] + '******' + usr_phone[-3:]), 'method': 'SMS'} elif self.verification_method == 'OTP App': - totp_uri = pyotp.TOTP(self.otp_secret).provisioning_uri(self.user, issuer_name="Estate Manager") + totp_uri = pyotp.TOTP(self.otp_secret).provisioning_uri(self.user, issuer_name=otp_issuer) if frappe.db.get_default(self.user + '_otplogin'): otp_setup_completed = True @@ -399,6 +400,7 @@ class LoginManager: clear_cookies() def send_token_via_sms(self, otpsecret, token=None, phone_no=None): + otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') try: from frappe.core.doctype.sms_settings.sms_settings import send_request except: @@ -412,7 +414,7 @@ class LoginManager: return False hotp = pyotp.HOTP(otpsecret) - args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token)))} + args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer} for d in ss.get("parameters"): args[d.parameter] = d.value @@ -423,13 +425,14 @@ class LoginManager: return True def send_token_via_email(self, token, otpsecret): + otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') user_email = frappe.db.get_value('User', self.user, 'email') if not user_email: return False hotp = pyotp.HOTP(otpsecret) email_args = { - 'recipients':user_email, 'sender':None, 'subject':'Verification Code', - 'message':'

Your verification code is {}

'.format(hotp.at(int(token))), + 'recipients':user_email, 'sender':None, 'subject':'Verification Code from {}'.format(otp_issuer or "Frappe Framework"), + 'message':'

Your verification code is {}.

'.format(hotp.at(int(token))), 'delayed':False, 'retry':3 } enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 7f2ab54e0c..c7c9d2174d 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -744,6 +744,38 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Frappe Framework", + "fieldname": "otp_issuer_name", + "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": "OTP Issuer Name", + "length": 0, + "no_copy": 0, + "options": "", + "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, @@ -1062,7 +1094,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-07 17:21:50.082744", + "modified": "2017-07-23 01:35:39.150010", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/test_system_settings.js b/frappe/core/doctype/system_settings/test_system_settings.js new file mode 100644 index 0000000000..53edaba99d --- /dev/null +++ b/frappe/core/doctype/system_settings/test_system_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: System Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('System Settings', [ + // insert a new System Settings + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 91477069ba..dfa7fde5c8 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -269,7 +269,8 @@ var continue_otp_app = function(setup, qrcode){ var qrcode_div = $('
').attr({'id':'qrcode_div','style':'text-align:center;padding-bottom:15px;'}); if (!setup){ - direction = $('
').attr('id','qr_info').text('Scan QR Code and enter the resulting code displayed' ), + direction = $('
').attr('id','qr_info').text('Scan QR Code and enter the resulting code displayed. \ + You can use apps such as Google Authenticator, Lastpass Authenticator, Authy, Duo Mobile and others.'), qrimg = $('').attr({ 'src':'data:image/svg+xml;base64,' + qrcode, 'style':'width:250px;height:250px;'}); From 2ad495ea27610ddd9eade38f7458e9127af342de Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Tue, 25 Jul 2017 02:09:25 +0100 Subject: [PATCH 19/67] Added ability for either Administrator or user to reset the OTP secret. A user that is not Administrator can only reset their own OTP secret. Administrator can reset OTP secret of any user. --- frappe/core/doctype/user/user.js | 9 +++++++++ frappe/core/doctype/user/user.py | 25 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 39423ae600..d809d3a059 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -73,6 +73,15 @@ frappe.ui.form.on('User', { } }) }) + + frm.add_custom_button(__("Reset OTP Secret"), function() { + frappe.call({ + method: "frappe.core.doctype.user.user.reset_otp_secret", + args: { + "user": frm.doc.name + } + }) + }) frm.trigger('enabled'); diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 0bcfe0ec2b..c8fc9bb738 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -14,6 +14,7 @@ import frappe.share import re from frappe.limits import get_limits from frappe.website.utils import is_signup_enabled +from frappe.utils.background_jobs import enqueue STANDARD_USERS = ("Guest", "Administrator") @@ -618,8 +619,8 @@ def get_email_awaiting(user): return waiting else: frappe.db.sql("""update `tabUser Email` - set awaiting_password =0 - where parent = %(user)s""",{"user":user}) + set awaiting_password =0 + where parent = %(user)s""",{"user":user}) return False @frappe.whitelist(allow_guest=False) @@ -707,7 +708,7 @@ def ask_pass_update(): from frappe.utils import set_default users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email` - WHERE awaiting_password = 1""", as_dict=True) + WHERE awaiting_password = 1""", as_dict=True) password_list = [ user.get("user") for user in users ] set_default("email_user_password", u','.join(password_list)) @@ -987,3 +988,21 @@ def send_token_via_email(tmp_id,token=None): delayed=False, retry=3) return True + +@frappe.whitelist(allow_guest=True) +def reset_otp_secret(user): + otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') + user_email = frappe.db.get_value('User',user, 'email') + if frappe.session.user in ["Administrator", user] : + frappe.defaults.clear_default(user + '_otplogin') + frappe.defaults.clear_default(user + '_otpsecret') + email_args = { + 'recipients':user_email, 'sender':None, 'subject':'OTP Secret Reset - {}'.format(otp_issuer or "Frappe Framework"), + 'message':'

Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.

'.format(otp_issuer or "Frappe Framework"), + 'delayed':False, + 'retry':3 + } + enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) + return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login.")) + else: + return frappe.throw(_("OTP secret can only be reset by the Administrator.")) \ No newline at end of file From a8b526bfd14b66e26183b912b51ea40389588cfd Mon Sep 17 00:00:00 2001 From: B H Boma Date: Wed, 26 Jul 2017 17:34:36 +0100 Subject: [PATCH 20/67] [WIP][Refactor] Redo twofactor code --- frappe/auth.py | 41 ++----- frappe/tests/test_twofactor.py | 92 ++++++++++++++ frappe/twofactor.py | 214 +++++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 28 deletions(-) create mode 100644 frappe/tests/test_twofactor.py create mode 100644 frappe/twofactor.py diff --git a/frappe/auth.py b/frappe/auth.py index f6823d7b2d..8e5c7bc875 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -17,6 +17,7 @@ from frappe.translate import get_lang_code from frappe.utils.password import check_password from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log from frappe.utils.background_jobs import enqueue +from twofactor import validate_2fa_if_set, confirm_otp_token from urllib import quote @@ -102,7 +103,7 @@ class LoginManager: self.user_type = None if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": - self.login() + if not self.login():return self.resume = False # run login triggers @@ -193,44 +194,28 @@ class LoginManager: frappe.local.response['verification'] = verification_obj frappe.local.response['tmp_id'] = tmp_id - raise frappe.RequestToken + # raise frappe.RequestToken else: self.post_login(no_two_auth=True) def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) + self.authenticate() + otp = validate_2fa_if_set(self.user) + return self.post_login() - otp = frappe.form_dict.get('otp') - if otp: - try: - 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']) - except: - pass - self.post_login() - else: - self.authenticate() - if (self.user != 'Administrator') and (frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') == unicode(1)): - self.process_2fa() - else: - self.post_login(no_two_auth=True) - def post_login(self,no_two_auth=False): + def post_login(self,otp=None): self.run_trigger('on_login') self.validate_ip_address() self.validate_hour() - if frappe.form_dict.get('otp') and not no_two_auth: - 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.set_user_info() - else: - self.make_session() - self.set_user_info() + if not confirm_otp_token(self,otp): + return False + self.make_session() + self.set_user_info() + return True + def confirm_token(self, otp=None, tmp_id=None, hotp_token=False): try: diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py new file mode 100644 index 0000000000..ef86b016d7 --- /dev/null +++ b/frappe/tests/test_twofactor.py @@ -0,0 +1,92 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals + +import unittest, frappe +from werkzeug.wrappers import Request +from werkzeug.test import EnvironBuilder +from frappe.auth import LoginManager, HTTPRequest +from frappe.website import render + + +class TestTwoFactor(unittest.TestCase): + + + def setUp(self): + self.http_requests = create_http_request() + self.login_manager = frappe.local.login_manager + self.user = self.login_manager.user + print self.user + + def test_debug(self): + pass + + # def test_two_factor_auth_user(self): + # '''Test OTP secret and verification method is initiated.''' + # two_factor_role = self.login_manager.two_factor_auth_user() + # otp_secret = frappe.db.get_default('test@erpnext.com_otpsecret') + # self.assertFalse(two_factor_role) + # toggle_2fa_all_role(True) + # two_factor_role = self.login_manager.two_factor_auth_user() + # self.assertTrue(two_factor_role) + # self.assertNotEqual(otp_secret,None) + # self.assertEqual(self.login_manager.verification_method,'OTP App') + # frappe.db.set_default('test@erpnext.com_otpsecret', None) + # toggle_2fa_all_role(False) + + # def test_get_verification_obj(self): + # '''Auth url should be present in verification object.''' + # verification_obj = self.login_manager.get_verification_obj() + # self.assertIn('otpauth://',verification_obj['totp_uri']) + # self.assertTrue(len(verification_obj['qrcode']) > 1 ) + + # def test_process_2fa(self): + # self.login_manager.process_2fa() + # toggle_2fa_all_role(True) + # print self.login_manager.info + # # print frappe.local.response['verification'] + # # self.assertTrue(False not in [i in frappe.local.response['verification'] \ + # # for i in ['totp_uri','method','qrcode','setup']]) + # toggle_2fa_all_role(False) + + # def test_confirm_token(self): + # pass + + # def test_send_token_via_sms(self): + # pass + + # def test_send_token_via_email(self): + # pass + + + +def set_request(**kwargs): + builder = EnvironBuilder(**kwargs) + frappe.local.request = Request(builder.get_environ()) + +def create_http_request(): + '''Get http request object.''' + set_request(method='POST', path='login') + enable_2fa() + frappe.form_dict['usr'] = 'test@erpnext.com' + frappe.form_dict['pwd'] = 'test' + frappe.local.form_dict['cmd'] = 'login' + http_requests = HTTPRequest() + return http_requests + +def enable_2fa(): + '''Enable Two factor in system settings.''' + system_settings = frappe.get_doc('System Settings') + system_settings.enable_two_factor_auth = True + system_settings.two_factor_method = 'OTP App' + system_settings.save(ignore_permissions=True) + frappe.db.commit() + +def toggle_2fa_all_role(state=None): + all_role = frappe.get_doc('Role','All') + if state == None: + state = False if all_role.two_factor_auth == True else False + if state not in [True,False]:return + all_role.two_factor_auth = state + all_role.save(ignore_permissions=True) + frappe.db.commit() diff --git a/frappe/twofactor.py b/frappe/twofactor.py new file mode 100644 index 0000000000..b05f29428b --- /dev/null +++ b/frappe/twofactor.py @@ -0,0 +1,214 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals + +import frappe +import pyotp,base64,os +from frappe.utils.background_jobs import enqueue +from pyqrcode import create as qrcreate +from StringIO import StringIO +from base64 import b64encode,b32encode + + + +def validate_2fa_if_set(user): + '''Check for 2fa if set in settings.''' + site_otp_enabled = frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') + user_otp_enabled = two_factor_is_enabled_for_(user) + #Don't validate for Admin of if not enabled + if user =='Administrator' or not site_otp_enabled or not user_otp_enabled: + return (None,None,None) + otp = frappe.form_dict.get('otp') + if otp: + user = frappe.cache().get(frappe.form_dict.get('tmp_id')+'_usr') + pwd = frappe.cache().get(frappe.form_dict.get('tmp_id')+'_pwd') + return (user,pwd,otp) + authenticate_for_2factor(user) + + +def authenticate_for_2factor(user): + '''Authenticate two factor for enabled user before login.''' + otp_secret = get_otpsecret_for_(user) + verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') + token = int(pyotp.TOTP(otp_secret).now()) + tmp_id = frappe.generate_hash(length=8) + cache_2fa_data(user,token,otp_secret,tmp_id) + verification_obj = get_verification_obj(user,token,otp_secret) + # Save data in local + frappe.local.response['verification'] = verification_obj + frappe.local.response['tmp_id'] = tmp_id + +def cache_2fa_data(user,token,otp_secret,tmp_id): + '''Cache and set expiry for data.''' + pwd = frappe.form_dict.get('pwd') + verification_method = get_verification_method() + + # set increased expiry time for SMS and Email + if verification_method in ['SMS', 'Email']: + expiry_time = 300 + frappe.cache().set(tmp_id + '_token', token) + frappe.cache().expire(tmp_id + '_token', expiry_time) + else: + expiry_time = 180 + for k,v in {'_usr':user,'_pwd':pwd,'_otp_secret':otp_secret}.iteritems(): + frappe.cache().set("{0}{1}".format(tmp_id,k),v) + frappe.cache().expire("{0}{1}".format(tmp_id,k),expiry_time) + +def two_factor_is_enabled_for_(user): + '''Check if 2factor is enabled for user.''' + if isinstance(user,basestring): + user = frappe.get_doc('User',user) + if user.roles: + query = """select name from `tabRole` where two_factor_auth=1 + and name in ("All",{0});""".format(', '.join('\"{}\"'.format(i.role) for \ + i in user.roles)) + if len(frappe.db.sql(query)) > 0: + return True + return False + +def get_otpsecret_for_(user): + '''Set OTP Secret for user even if not set.''' + otp_secret = frappe.db.get_default(user + '_otpsecret') + if not otp_secret: + otp_secret = b32encode(os.urandom(10)).decode('utf-8') + frappe.db.set_default(user + '_otpsecret', otp_secret) + frappe.db.commit() + return otp_secret + +def get_verification_method(): + return frappe.db.get_value('System Settings', None, 'two_factor_method') + + + +def confirm_otp_token(login_manager, otp): + '''Confirm otp matches.''' + if not otp: + if two_factor_is_enabled_for_(login_manager.user): + return False + return True + hotp_token = frappe.cache().get(frappe.form_dict.get('tmp_id') + '_token') + tmp_id = frappe.form_dict.get('tmp_id') + otp_secret = frappe.cache().get(tmp_id + '_otp_secret') + if not otp_secret: + frappe.throw('Login session expired. Refresh page to try again') + hotp = pyotp.HOTP(otp_secret) + if hotp_token: + if hotp.verify(otp, int(hotp_token)): + frappe.cache().delete(tmp_id + '_token') + return + else: + login_manager.fail('Incorrect Verification code', login_manager.user) + + totp = pyotp.TOTP(otp_secret) + if totp.verify(otp): + # show qr code only once + if not frappe.db.get_default(login_manager.user + '_otplogin'): + frappe.db.set_default(login_manager.user + '_otplogin', 1) + return True + else: + login_manager.fail('Incorrect Verification code', user) + + +def get_verification_obj(user,token,otp_secret): + otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') + verification_method = get_verification_method() + verification_obj = None + if verification_method == 'SMS': + verification_obj = process_2fa_for_sms(user,token,otp_secret) + elif verification_method == 'OTP App': + verification_obj = process_2fa_for_otp_app(user,otp_secret,otp_issuer) + elif verification_method == 'Email': + process_2fa_for_email(user,token,otp_secret,otp_issuer) + return verification_obj + + +def process_2fa_for_sms(user,token,otp_secret): + '''Process sms method for 2fa.''' + phone = frappe.db.get_value('User', user, ['phone','mobile_no'], as_dict=1) + phone = phone.mobile_no or phone.phone + status = send_token_via_sms(otp_secret,token=token, phone_no=phone) + verification_obj = {'token_delivery': status, + 'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]), + 'method': 'SMS'} + return verification_obj + +def process_2fa_for_otp_app(user,otp_secret,otp_issuer): + '''Process OTP App method for 2fa.''' + totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) + if frappe.db.get_default(user + '_otplogin'): + otp_setup_completed = True + else: + otp_setup_completed = False + + verification_obj = {'totp_uri': totp_uri, + 'method': 'OTP App', + 'qrcode': get_qr_svg_code(totp_uri), + 'setup': otp_setup_completed } + return verification_obj + +def process_2fa_for_email(user,token,otp_secret,otp_issuer): + '''Process Email method for 2fa.''' + status = send_token_via_email(user,token,otp_secret,otp_issuer) + verification_obj = {'token_delivery': status, + 'prompt': status and 'Enter verification code sent to your registered email address', + 'method': 'Email'} + return verification_obj + + + +def send_token_via_sms(self, otpsecret, token=None, phone_no=None): + '''Send token as sms to user.''' + otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') + try: + from frappe.core.doctype.sms_settings.sms_settings import send_request + except: + return False + + if not phone_no: + return False + + ss = frappe.get_doc('SMS Settings', 'SMS Settings') + if not ss.sms_gateway_url: + return False + + hotp = pyotp.HOTP(otpsecret) + args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer} + for d in ss.get("parameters"): + args[d.parameter] = d.value + + args[ss.receiver_parameter] = phone_no + + sms_args = {'gateway_url':ss.sms_gateway_url,'params':args} + enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args) + return True + +def send_token_via_email(user, token, otp_secret, otp_issuer): + '''Send token to user as email.''' + user_email = frappe.db.get_value('User', user, 'email') + if not user_email: + return False + hotp = pyotp.HOTP(otp_secret) + email_args = { + 'recipients':user_email, 'sender':None, 'subject':'Verification Code from {}'.format(otp_issuer or "Frappe Framework"), + 'message':'

Your verification code is {}.

'.format(hotp.at(int(token))), + 'delayed':False, 'retry':3 } + + enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) + return True + + + +def get_qr_svg_code(totp_uri): + '''Get SVG code to display Qrcode for OTP.''' + url = qrcreate(totp_uri) + stream = StringIO() + url.svg(stream, scale=3) + svg = stream.getvalue().replace('\n','') + svg = b64encode(bytes(svg)) + return svg + + + + + From 68251a61128f1ea001437584266b016031a4fada Mon Sep 17 00:00:00 2001 From: B H Boma Date: Thu, 27 Jul 2017 01:12:22 +0100 Subject: [PATCH 21/67] [WIP][Refactor] Redo twofactor code --- frappe/auth.py | 180 ++---------------- .../system_settings/system_settings.json | 33 +++- frappe/core/doctype/user/user.py | 32 ---- frappe/templates/includes/login/login.js | 33 ++-- frappe/tests/test_twofactor.py | 114 ++++++----- frappe/twofactor.py | 91 ++++++--- 6 files changed, 197 insertions(+), 286 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 8e5c7bc875..58bdf1e53c 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -17,7 +17,8 @@ from frappe.translate import get_lang_code from frappe.utils.password import check_password from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log from frappe.utils.background_jobs import enqueue -from twofactor import validate_2fa_if_set, confirm_otp_token +from twofactor import should_run_2fa, authenticate_for_2factor, \ + confirm_otp_token,get_cached_user_pass from urllib import quote @@ -67,6 +68,7 @@ class HTTPRequest: def validate_csrf_token(self): if frappe.local.request and frappe.local.request.method=="POST": + if not frappe.local.session:return if not frappe.local.session.data.csrf_token \ or frappe.local.session.data.device=="mobile" \ or frappe.conf.get('ignore_csrf', None): @@ -103,7 +105,7 @@ class LoginManager: self.user_type = None if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": - if not self.login():return + if self.login()==False:return self.resume = False # run login triggers @@ -118,129 +120,26 @@ class LoginManager: self.make_session() self.set_user_info() - def two_factor_auth_user(self): - ''' Check if user has 2fa role and set otpsecret and verification method''' - two_factor_user_role = 0 - user_obj = frappe.get_doc('User', self.user) - if user_obj.roles: - query = """select name from `tabRole` where two_factor_auth=1 - and name in ("All",{0}) limit 1""".format(', '.join('\"{}\"'.format(i.role) for \ - i in user_obj.roles)) - two_factor_user_role = len(frappe.db.sql(query)) - - self.otp_secret = frappe.db.get_default(self.user + '_otpsecret') - if not self.otp_secret: - self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') - frappe.db.set_default(self.user + '_otpsecret', self.otp_secret) - frappe.db.commit() - - self.verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') - - return bool(two_factor_user_role) - - def get_verification_obj(self): - otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') - if self.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=self.token, phone_no=usr_phone, otpsecret=self.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 self.verification_method == 'OTP App': - totp_uri = pyotp.TOTP(self.otp_secret).provisioning_uri(self.user, issuer_name=otp_issuer) - - if frappe.db.get_default(self.user + '_otplogin'): - otp_setup_completed = True - else: - otp_setup_completed = False - - verification_obj = {'totp_uri': totp_uri, - 'method': 'OTP App', - 'qrcode': get_qr_svg_code(totp_uri), - 'setup': otp_setup_completed } - elif self.verification_method == 'Email': - status = self.send_token_via_email(token=self.token, otpsecret=self.otp_secret) - verification_obj = {'token_delivery': status, - 'prompt': status and 'Enter verification code sent to your registered email address', - 'method': 'Email'} - return verification_obj - - def process_2fa(self): - if self.two_factor_auth_user(): - self.token = int(pyotp.TOTP(self.otp_secret).now()) - verification_obj = self.get_verification_obj() - - tmp_id = frappe.generate_hash(length=8) - usr = frappe.form_dict.get('usr') - pwd = frappe.form_dict.get('pwd') - - # set increased expiry time for SMS and Email - if self.verification_method in ['SMS', 'Email']: - expiry_time = 300 - frappe.cache().set(tmp_id + '_token', self.token) - frappe.cache().expire(tmp_id + '_token', expiry_time) - else: - expiry_time = 180 - - frappe.cache().set(tmp_id + '_usr', usr) - frappe.cache().set(tmp_id + '_pwd', pwd) - frappe.cache().set(tmp_id + '_otp_secret', self.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, expiry_time) - - frappe.local.response['verification'] = verification_obj - frappe.local.response['tmp_id'] = tmp_id - - # raise frappe.RequestToken - else: - self.post_login(no_two_auth=True) def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) - self.authenticate() - otp = validate_2fa_if_set(self.user) - return self.post_login() + user,pwd = get_cached_user_pass() + self.authenticate(user=user,pwd=pwd) + if should_run_2fa(self.user): + authenticate_for_2factor(self.user) + if not confirm_otp_token(self): + return False + self.post_login() + - def post_login(self,otp=None): + def post_login(self): self.run_trigger('on_login') self.validate_ip_address() self.validate_hour() - if not confirm_otp_token(self,otp): - return False self.make_session() self.set_user_info() - return True - - - def confirm_token(self, otp=None, tmp_id=None, hotp_token=False): - try: - otp_secret = frappe.cache().get(tmp_id + '_otp_secret') - if not otp_secret: - frappe.throw('Login session expired. Refresh page to try again') - except AttributeError: - return False - - if hotp_token: - u_hotp = pyotp.HOTP(otp_secret) - if u_hotp.verify(otp, int(hotp_token)): - frappe.cache().delete(tmp_id + '_token') - return True - else: - self.fail('Incorrect Verification code', self.user) - - totp = pyotp.TOTP(otp_secret) - if totp.verify(otp): - # show qr code only once - if not frappe.db.get_default(self.user + '_otplogin'): - frappe.db.set_default(self.user + '_otplogin', 1) - return True - else: - self.fail('Incorrect Verification code', self.user) def set_user_info(self, resume=False): # set sid again @@ -384,45 +283,6 @@ class LoginManager: def clear_cookies(self): clear_cookies() - def send_token_via_sms(self, otpsecret, token=None, phone_no=None): - otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') - try: - from frappe.core.doctype.sms_settings.sms_settings import send_request - except: - return False - - if not phone_no: - return False - - ss = frappe.get_doc('SMS Settings', 'SMS Settings') - if not ss.sms_gateway_url: - return False - - hotp = pyotp.HOTP(otpsecret) - args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer} - for d in ss.get("parameters"): - args[d.parameter] = d.value - - args[ss.receiver_parameter] = phone_no - - sms_args = {'gateway_url':ss.sms_gateway_url,'params':args} - enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args) - return True - - def send_token_via_email(self, token, otpsecret): - otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') - user_email = frappe.db.get_value('User', self.user, 'email') - if not user_email: - return False - hotp = pyotp.HOTP(otpsecret) - email_args = { - 'recipients':user_email, 'sender':None, 'subject':'Verification Code from {}'.format(otp_issuer or "Frappe Framework"), - 'message':'

Your verification code is {}.

'.format(hotp.at(int(token))), - 'delayed':False, 'retry':3 } - - enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) - return True - class CookieManager: def __init__(self): self.cookies = {} @@ -473,16 +333,4 @@ def get_website_user_home_page(user): home_page = frappe.get_attr(home_page_method[-1])(user) return '/' + home_page.strip('/') else: - return '/me' - -def get_qr_svg_code(totp_uri): - '''Get SVG code to display Qrcode for OTP.''' - from pyqrcode import create as qrcreate - from StringIO import StringIO - from base64 import b64encode - url = qrcreate(totp_uri) - stream = StringIO() - url.svg(stream, scale=3) - svg = stream.getvalue().replace('\n','') - svg = b64encode(bytes(svg)) - return svg + return '/me' \ No newline at end of file diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index c7c9d2174d..d8a4de1d9d 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -962,6 +962,37 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.two_factor_method == \"OTP App\"", + "fieldname": "send_barcode_as_email", + "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": "Send Barcode 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, @@ -1094,7 +1125,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-23 01:35:39.150010", + "modified": "2017-07-26 18:31:27.992012", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index c8fc9bb738..f90801056d 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -479,38 +479,6 @@ class User(Document): if len(email_accounts) != len(set(email_accounts)): 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() def get_timezones(): import pytz diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index dfa7fde5c8..e3e0537b14 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -156,23 +156,7 @@ login.login_handlers = (function() { var login_handlers = { 200: function(data) { - console.log(data); - if(data.verification) { - login.set_indicator("{{ _("Success") }}", 'green'); - - document.cookie = "tmp_id="+data.tmp_id; - - if (data.verification.method == 'OTP App'){ - continue_otp_app(data.verification.setup, data.verification.qrcode); - } else if (data.verification.method == 'SMS'){ - continue_sms(data.verification.setup, data.verification.prompt); - } else if (data.verification.method == 'Email'){ - continue_email(data.verification.setup, data.verification.prompt); - } - - return false; - - } else if(data.message == 'Logged In'){ + if(data.message == 'Logged In'){ login.set_indicator("{{ _("Success") }}", 'green'); window.location.href = get_url_arg("redirect-to") || data.home_page; } else if(data.message=="No App") { @@ -212,6 +196,21 @@ login.login_handlers = (function() { } //login.set_indicator(__(data.message), 'green'); } + + //OTP verification + if(data.verification) { + login.set_indicator("{{ _("Success") }}", 'green'); + + document.cookie = "tmp_id="+data.tmp_id; + + if (data.verification.method == 'OTP App'){ + continue_otp_app(data.verification.setup, data.verification.qrcode); + } else if (data.verification.method == 'SMS'){ + continue_sms(data.verification.setup, data.verification.prompt); + } else if (data.verification.method == 'Email'){ + continue_email(data.verification.setup, data.verification.prompt); + } + } }, 401: get_error_handler("{{ _("Invalid Login. Try again.") }}"), 417: get_error_handler("{{ _("Oops! Something went wrong") }}") diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index ef86b016d7..4dac0774c2 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -2,11 +2,13 @@ # MIT License. See license.txt from __future__ import unicode_literals -import unittest, frappe +import unittest, frappe, pyotp from werkzeug.wrappers import Request from werkzeug.test import EnvironBuilder from frappe.auth import LoginManager, HTTPRequest -from frappe.website import render +from frappe.twofactor import * +import time + class TestTwoFactor(unittest.TestCase): @@ -16,48 +18,69 @@ class TestTwoFactor(unittest.TestCase): self.http_requests = create_http_request() self.login_manager = frappe.local.login_manager self.user = self.login_manager.user - print self.user - - def test_debug(self): - pass - - # def test_two_factor_auth_user(self): - # '''Test OTP secret and verification method is initiated.''' - # two_factor_role = self.login_manager.two_factor_auth_user() - # otp_secret = frappe.db.get_default('test@erpnext.com_otpsecret') - # self.assertFalse(two_factor_role) - # toggle_2fa_all_role(True) - # two_factor_role = self.login_manager.two_factor_auth_user() - # self.assertTrue(two_factor_role) - # self.assertNotEqual(otp_secret,None) - # self.assertEqual(self.login_manager.verification_method,'OTP App') - # frappe.db.set_default('test@erpnext.com_otpsecret', None) - # toggle_2fa_all_role(False) - - # def test_get_verification_obj(self): - # '''Auth url should be present in verification object.''' - # verification_obj = self.login_manager.get_verification_obj() - # self.assertIn('otpauth://',verification_obj['totp_uri']) - # self.assertTrue(len(verification_obj['qrcode']) > 1 ) - - # def test_process_2fa(self): - # self.login_manager.process_2fa() - # toggle_2fa_all_role(True) - # print self.login_manager.info - # # print frappe.local.response['verification'] - # # self.assertTrue(False not in [i in frappe.local.response['verification'] \ - # # for i in ['totp_uri','method','qrcode','setup']]) - # toggle_2fa_all_role(False) - - # def test_confirm_token(self): - # pass - - # def test_send_token_via_sms(self): - # pass - - # def test_send_token_via_email(self): - # pass + def tearDown(self): + tmp_id = frappe.local.response['tmp_id'] + frappe.local.response['verification'] = None + frappe.local.response['tmp_id'] = None + frappe.clear_cache(user=self.user) + + + def test_should_run_2fa(self): + '''Should return true if enabled.''' + toggle_2fa_all_role(state=True) + self.assertTrue(should_run_2fa(self.user)) + toggle_2fa_all_role(state=False) + self.assertFalse(should_run_2fa(self.user)) + + def test_get_cached_user_pass(self): + '''Cached data should not contain user and pass before 2fa.''' + user,pwd = get_cached_user_pass() + self.assertTrue(all([not user, not pwd])) + + def test_authenticate_for_2factor(self): + '''Verification obj and tmp_id should be set in frappe.local.''' + authenticate_for_2factor(self.user) + verification_obj = frappe.local.response['verification'] + tmp_id = frappe.local.response['tmp_id'] + self.assertTrue(verification_obj) + self.assertTrue(tmp_id) + for k in ['_usr','_pwd','_otp_secret']: + self.assertTrue(frappe.cache().get('{0}{1}'.format(tmp_id,k)), + '{} not available'.format(k)) + + def test_two_factor_is_enabled_for_user(self): + '''Should be true if enabled for user.''' + toggle_2fa_all_role(state=True) + self.assertTrue(two_factor_is_enabled_for_(self.user)) + toggle_2fa_all_role(state=False) + self.assertFalse(two_factor_is_enabled_for_(self.user)) + + def test_get_otpsecret_for_user(self): + '''OTP secret should be set for user.''' + self.assertTrue(get_otpsecret_for_(self.user)) + self.assertTrue(frappe.db.get_default(self.user + '_otpsecret')) + + def test_confirm_otp_token(self): + '''Ensure otp is confirmed''' + authenticate_for_2factor(self.user) + tmp_id = frappe.local.response['tmp_id'] + otp = 'wrongotp' + with self.assertRaises(frappe.AuthenticationError): + confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id) + otp = get_otp(self.user) + self.assertTrue(confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)) + if frappe.flags.tests_verbose: + print('Sleeping for 30secs to confirm token expires..') + time.sleep(30) + with self.assertRaises(frappe.AuthenticationError): + confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id) + + def test_get_verification_obj(self): + '''Confirm verification object is returned.''' + otp_secret = get_otpsecret_for_(self.user) + token = int(pyotp.TOTP(otp_secret).now()) + self.assertTrue(get_verification_obj(self.user,token,otp_secret)) def set_request(**kwargs): @@ -90,3 +113,8 @@ def toggle_2fa_all_role(state=None): all_role.two_factor_auth = state all_role.save(ignore_permissions=True) frappe.db.commit() + +def get_otp(user): + otp_secret = get_otpsecret_for_(user) + otp = pyotp.TOTP(otp_secret) + return otp.now() \ No newline at end of file diff --git a/frappe/twofactor.py b/frappe/twofactor.py index b05f29428b..4ddfc9b981 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from frappe import _ import pyotp,base64,os from frappe.utils.background_jobs import enqueue from pyqrcode import create as qrcreate @@ -11,20 +12,26 @@ from StringIO import StringIO from base64 import b64encode,b32encode +class ExpiredLoginExpection(Exception):pass -def validate_2fa_if_set(user): - '''Check for 2fa if set in settings.''' +def should_run_2fa(user): + '''Check if 2fa should run.''' site_otp_enabled = frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') user_otp_enabled = two_factor_is_enabled_for_(user) #Don't validate for Admin of if not enabled if user =='Administrator' or not site_otp_enabled or not user_otp_enabled: - return (None,None,None) - otp = frappe.form_dict.get('otp') - if otp: - user = frappe.cache().get(frappe.form_dict.get('tmp_id')+'_usr') - pwd = frappe.cache().get(frappe.form_dict.get('tmp_id')+'_pwd') - return (user,pwd,otp) - authenticate_for_2factor(user) + return False + return True + + +def get_cached_user_pass(): + '''Get user and password if set.''' + user = pwd = None + tmp_id = frappe.form_dict.get('tmp_id') + if tmp_id: + user = frappe.cache().get(tmp_id+'_usr') + pwd = frappe.cache().get(tmp_id+'_pwd') + return (user,pwd) def authenticate_for_2factor(user): @@ -81,24 +88,27 @@ def get_verification_method(): -def confirm_otp_token(login_manager, otp): - '''Confirm otp matches.''' +def confirm_otp_token(login_manager,otp=None,tmp_id=None): + '''Confirm otp matches.''' + if not otp: + otp = frappe.form_dict.get('otp') if not otp: if two_factor_is_enabled_for_(login_manager.user): return False return True - hotp_token = frappe.cache().get(frappe.form_dict.get('tmp_id') + '_token') - tmp_id = frappe.form_dict.get('tmp_id') + if not tmp_id: + tmp_id = frappe.form_dict.get('tmp_id') + hotp_token = frappe.cache().get(tmp_id + '_token') otp_secret = frappe.cache().get(tmp_id + '_otp_secret') if not otp_secret: - frappe.throw('Login session expired. Refresh page to try again') + raise ExpiredLoginExpection(_('Login session expired, refresh page to retry')) hotp = pyotp.HOTP(otp_secret) if hotp_token: if hotp.verify(otp, int(hotp_token)): frappe.cache().delete(tmp_id + '_token') - return + return True else: - login_manager.fail('Incorrect Verification code', login_manager.user) + login_manager.fail(_('Incorrect Verification code'), login_manager.user) totp = pyotp.TOTP(otp_secret) if totp.verify(otp): @@ -107,7 +117,7 @@ def confirm_otp_token(login_manager, otp): frappe.db.set_default(login_manager.user + '_otplogin', 1) return True else: - login_manager.fail('Incorrect Verification code', user) + login_manager.fail('Incorrect Verification code', login_manager.user) def get_verification_obj(user,token,otp_secret): @@ -117,7 +127,10 @@ def get_verification_obj(user,token,otp_secret): if verification_method == 'SMS': verification_obj = process_2fa_for_sms(user,token,otp_secret) elif verification_method == 'OTP App': - verification_obj = process_2fa_for_otp_app(user,otp_secret,otp_issuer) + if should_send_barcode_as_email(): + verification_obj = process_2fa_for_email(user,token,otp_secret,otp_issuer,method='otp_app') + else: + verification_obj = process_2fa_for_otp_app(user,otp_secret,otp_issuer) elif verification_method == 'Email': process_2fa_for_email(user,token,otp_secret,otp_issuer) return verification_obj @@ -147,17 +160,26 @@ def process_2fa_for_otp_app(user,otp_secret,otp_issuer): 'setup': otp_setup_completed } return verification_obj -def process_2fa_for_email(user,token,otp_secret,otp_issuer): +def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='email'): '''Process Email method for 2fa.''' - status = send_token_via_email(user,token,otp_secret,otp_issuer) + message = None + status = True + # TODO SVG don't display in email + 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) + message = '''

Please scan the barcode for One Time Password

+ '''.format(get_qr_svg_code(totp_uri)) + if method == 'email' or message: + status = send_token_via_email(user,token,otp_secret,otp_issuer,message=message) verification_obj = {'token_delivery': status, - 'prompt': status and 'Enter verification code sent to your registered email address', + 'prompt': status and 'Verification code has been sent to your registered email address', 'method': 'Email'} return verification_obj -def send_token_via_sms(self, otpsecret, token=None, phone_no=None): +def send_token_via_sms(otpsecret, token=None, phone_no=None): '''Send token as sms to user.''' otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') try: @@ -183,29 +205,44 @@ def send_token_via_sms(self, 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) return True -def send_token_via_email(user, token, otp_secret, otp_issuer): +def send_token_via_email(user, token, otp_secret, otp_issuer,message=None): '''Send token to user as email.''' user_email = frappe.db.get_value('User', user, 'email') if not user_email: return False hotp = pyotp.HOTP(otp_secret) + if not message: + message = '

Your verification code is {}.

'.format(hotp.at(int(token))) email_args = { 'recipients':user_email, 'sender':None, 'subject':'Verification Code from {}'.format(otp_issuer or "Frappe Framework"), - 'message':'

Your verification code is {}.

'.format(hotp.at(int(token))), + 'message':message, 'delayed':False, 'retry':3 } enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) return True +def should_send_barcode_as_email(): + settings = frappe.get_doc('System Settings', 'System Settings') + if settings.two_factor_method and settings.send_barcode_as_email: + return True + return False + +def send_barcode_as_email(user,svg_code): + pass + def get_qr_svg_code(totp_uri): '''Get SVG code to display Qrcode for OTP.''' url = qrcreate(totp_uri) + svg = '' stream = StringIO() - url.svg(stream, scale=3) - svg = stream.getvalue().replace('\n','') - svg = b64encode(bytes(svg)) + try: + url.svg(stream, scale=3) + svg = stream.getvalue().replace('\n','') + svg = b64encode(bytes(svg)) + finally: + stream.close() return svg From dcee43f646f251c36bfd9ab49cdf61d2d034f0e2 Mon Sep 17 00:00:00 2001 From: B H Boma Date: Thu, 27 Jul 2017 17:59:43 +0100 Subject: [PATCH 22/67] Settings to send Qrcode as email to user --- .../system_settings/system_settings.json | 36 +++++++++- frappe/hooks.py | 3 +- frappe/templates/includes/login/login.js | 2 +- frappe/twofactor.py | 67 ++++++++++++++++--- requirements.txt | 1 + 5 files changed, 95 insertions(+), 14 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index d8a4de1d9d..77f207cb10 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -744,6 +744,38 @@ "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", + "description": "Time in seconds to retain barcode image on server. Min:240", + "fieldname": "lifespan_barcode_image", + "fieldtype": "Int", + "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": "Delete Barcode Image On server", + "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, @@ -968,7 +1000,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval:doc.two_factor_method == \"OTP App\"", + "depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"OTP App\"", "fieldname": "send_barcode_as_email", "fieldtype": "Check", "hidden": 0, @@ -1125,7 +1157,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-26 18:31:27.992012", + "modified": "2017-07-27 12:23:01.135841", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/hooks.py b/frappe/hooks.py index 49ec772175..bf990a9f72 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -128,7 +128,8 @@ scheduler_events = { "frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.notify_unreplied", "frappe.oauth.delete_oauth2_data", - "frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment" + "frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment", + "frappe.twofactor.delete_all_barcodes_for_users" ], "hourly": [ "frappe.model.utils.link_count.update_link_count", diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index e3e0537b14..1e59e40175 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -198,7 +198,7 @@ login.login_handlers = (function() { } //OTP verification - if(data.verification) { + if(data.verification && data.message != 'Logged In') { login.set_indicator("{{ _("Success") }}", 'green'); document.cookie = "tmp_id="+data.tmp_id; diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 4ddfc9b981..ffae19a548 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -10,6 +10,7 @@ from frappe.utils.background_jobs import enqueue from pyqrcode import create as qrcreate from StringIO import StringIO from base64 import b64encode,b32encode +from frappe.utils import get_url, get_datetime, time_diff_in_seconds class ExpiredLoginExpection(Exception):pass @@ -114,7 +115,8 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None): if totp.verify(otp): # show qr code only once if not frappe.db.get_default(login_manager.user + '_otplogin'): - frappe.db.set_default(login_manager.user + '_otplogin', 1) + # frappe.db.set_default(login_manager.user + '_otplogin', 1) + delete_qrimage(login_manager.user) return True else: login_manager.fail('Incorrect Verification code', login_manager.user) @@ -168,8 +170,8 @@ def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='email'): 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) message = '''

Please scan the barcode for One Time Password

- '''.format(get_qr_svg_code(totp_uri)) + '''.format(qrcode_as_png(user,totp_uri)) if method == 'email' or message: status = send_token_via_email(user,token,otp_secret,otp_issuer,message=message) verification_obj = {'token_delivery': status, @@ -227,10 +229,6 @@ def should_send_barcode_as_email(): return True return False -def send_barcode_as_email(user,svg_code): - pass - - def get_qr_svg_code(totp_uri): '''Get SVG code to display Qrcode for OTP.''' @@ -245,7 +243,56 @@ def get_qr_svg_code(totp_uri): stream.close() return svg - - - +def qrcode_as_png(user,totp_uri): + '''Save temporary Qrcode to server.''' + from frappe.utils.file_manager import save_file + folder = create_barcode_folder() + png_file_name = '{}.png'.format(frappe.generate_hash(length=20)) + file_obj = save_file(png_file_name,png_file_name,'User',user,folder=folder) + frappe.db.commit() + file_url = get_url(file_obj.file_url) + file_path = os.path.join(frappe.get_site_path('public', 'files'),file_obj.file_name) + url = qrcreate(totp_uri) + with open(file_path,'w') as png_file: + url.png(png_file,scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc]) + return file_url + +def create_barcode_folder(): + '''Get Barcodes folder.''' + folder_name = 'Barcodes' + folder = frappe.db.exists('File',{'file_name':folder_name}) + if folder: + return folder + folder = frappe.get_doc({ + 'doctype':'File', + 'file_name':folder_name, + 'is_folder':1, + 'folder':'Home' + }) + folder.insert(ignore_permissions=True) + return folder.name + +def delete_qrimage(user,check_expiry=False): + '''Delete Qrimage when user logs in.''' + user_barcodes = frappe.get_all('File',{'attached_to_doctype':'User', + 'attached_to_name':user,'folder':'Home/Barcodes'}) + for barcode in user_barcodes: + if check_expiry and not should_remove_barcode_image(barcode):continue + barcode = frappe.get_doc('File',barcode.name) + frappe.delete_doc('File',barcode.name,ignore_permissions=True) + +def delete_all_barcodes_for_users(): + '''Task to delete all barcodes for user.''' + users = frappe.get_all('User',{'enabled':1}) + for user in users: + delete_qrimage(user.name,check_expiry=True) + +def should_remove_barcode_image(barcode): + '''Check if it's time to delete barcode image from server. ''' + if isinstance(barcode, basestring): + barcode = frappe.get_doc('File',barcode) + lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_barcode_image') + if time_diff_in_seconds(get_datetime(),barcode.creation) > int(lifespan): + return True + return False diff --git a/requirements.txt b/requirements.txt index 01054a3870..0f6a4ef421 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,5 +43,6 @@ pypdf openpyxl pyotp pyqrcode +pypng premailer From 636792489cda22f5a80255caaf55f01935ff6828 Mon Sep 17 00:00:00 2001 From: B H Boma Date: Thu, 27 Jul 2017 18:02:26 +0100 Subject: [PATCH 23/67] [fix]Prevent future of qrcode if user login --- frappe/twofactor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/twofactor.py b/frappe/twofactor.py index ffae19a548..a70f3d1985 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -115,7 +115,7 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None): if totp.verify(otp): # show qr code only once if not frappe.db.get_default(login_manager.user + '_otplogin'): - # frappe.db.set_default(login_manager.user + '_otplogin', 1) + frappe.db.set_default(login_manager.user + '_otplogin', 1) delete_qrimage(login_manager.user) return True else: From 94cc69dfa576d1f216e2215c0d67941c36ac5251 Mon Sep 17 00:00:00 2001 From: B H Boma Date: Fri, 28 Jul 2017 17:48:36 +0100 Subject: [PATCH 24/67] [WIP]Add QRCode email feature --- .../system_settings/system_settings.json | 132 +++++++++++++++++- frappe/twofactor.py | 84 +++++++++-- frappe/www/qrcode.html | 12 ++ frappe/www/qrcode.py | 43 ++++++ 4 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 frappe/www/qrcode.html create mode 100644 frappe/www/qrcode.py diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 77f207cb10..02e5eda0e5 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -751,7 +751,7 @@ "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", - "description": "Time in seconds to retain barcode image on server. Min:240", + "description": "Time in seconds to retain QR code image on server. Min:240", "fieldname": "lifespan_barcode_image", "fieldtype": "Int", "hidden": 0, @@ -761,7 +761,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Delete Barcode Image On server", + "label": "Delete QR Code Image On server", "length": 0, "no_copy": 0, "permlevel": 0, @@ -1010,7 +1010,131 @@ "in_global_search": 0, "in_list_view": 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, "no_copy": 0, "permlevel": 0, @@ -1157,7 +1281,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-27 12:23:01.135841", + "modified": "2017-07-28 07:21:12.520227", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/twofactor.py b/frappe/twofactor.py index a70f3d1985..3bcf8c5ce1 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -7,19 +7,20 @@ import frappe from frappe import _ import pyotp,base64,os from frappe.utils.background_jobs import enqueue +from jinja2 import Template from pyqrcode import create as qrcreate from StringIO import StringIO from base64 import b64encode,b32encode 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): '''Check if 2fa should run.''' site_otp_enabled = frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') 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: return False 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') otp_secret = frappe.cache().get(tmp_id + '_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) if 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) return True 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): @@ -166,20 +167,71 @@ def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='email'): '''Process Email method for 2fa.''' message = None status = True - # TODO SVG don't display in email 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) - message = '''

Please scan the barcode for One Time Password

- '''.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: - 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, 'prompt': status and 'Verification code has been sent to your registered email address', 'method': 'Email'} 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
{{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
{{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): '''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) 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.''' user_email = frappe.db.get_value('User', user, 'email') if not user_email: return False 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: - message = '

Your verification code is {}.

'.format(hotp.at(int(token))) + message = get_email_body_for_2fa(template_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, 'delayed':False, 'retry':3 } @@ -236,7 +292,7 @@ def get_qr_svg_code(totp_uri): svg = '' stream = StringIO() try: - url.svg(stream, scale=3) + url.svg(stream, scale=4, background="#eee", module_color="#222") svg = stream.getvalue().replace('\n','') svg = b64encode(bytes(svg)) finally: diff --git a/frappe/www/qrcode.html b/frappe/www/qrcode.html new file mode 100644 index 0000000000..3edd1f7e18 --- /dev/null +++ b/frappe/www/qrcode.html @@ -0,0 +1,12 @@ +
+
+
+ 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. + +
+
+ +
+
+
\ No newline at end of file diff --git a/frappe/www/qrcode.py b/frappe/www/qrcode.py new file mode 100644 index 0000000000..f87ede7597 --- /dev/null +++ b/frappe/www/qrcode.py @@ -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) From 6b06f9e94313f13cd59a0cb70eca91d32f3ec5c0 Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Mon, 31 Jul 2017 00:02:53 +0100 Subject: [PATCH 25/67] Various fixes. Barcode email now sent only once instead of on each login. --- .../system_settings/system_settings.json | 128 +----------------- frappe/templates/includes/login/login.js | 58 +------- frappe/twofactor.py | 47 +++---- frappe/www/qrcode.html | 37 +++-- frappe/www/qrcode.py | 4 - 5 files changed, 59 insertions(+), 215 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 02e5eda0e5..338974cc47 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1025,130 +1025,6 @@ "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, - "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, @@ -1281,8 +1157,8 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-28 07:21:12.520227", - "modified_by": "Administrator", + "modified": "2017-07-29 13:33:49.201189", + "modified_by": "chude.osiegbu@manqala.com", "module": "Core", "name": "System Settings", "name_case": "", diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 215dcfbc2e..cd65cdb250 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -269,18 +269,12 @@ var continue_otp_app = function(setup, qrcode){ request_otp(); var qrcode_div = $('
').attr({'id':'qrcode_div','style':'text-align:center;padding-bottom:15px;'}); - if (!setup){ - direction = $('
').attr('id','qr_info').text('Scan QR Code and enter the resulting code displayed. \ - You can use apps such as Google Authenticator, Lastpass Authenticator, Authy, Duo Mobile and others.'), - qrimg = $('').attr({ - 'src':'data:image/svg+xml;base64,' + qrcode, - 'style':'width:250px;height:250px;'}); - + if (setup){ + direction = $('
').attr('id','qr_info').text('Enter Code displayed in OTP App.'); qrcode_div.append(direction); - qrcode_div.append(qrimg); $('#otp_div').prepend(qrcode_div); } else { - direction = $('
').attr('id','qr_info').text('Enter Code displayed in OTP App'); + direction = $('
').attr('id','qr_info').text('OTP setup using OTP App was not completed. Please contact Administrator.'); qrcode_div.append(direction); $('#otp_div').prepend(qrcode_div); } @@ -291,36 +285,10 @@ var continue_sms = function(setup, prompt){ var sms_div = $('
').attr({'id':'sms_div','style':'padding-bottom:15px;text-align:center;'}); if (setup){ - direction = $('
').attr('id','sms_info').text('Enter phone number to send verification code'); - sms_div.append(direction); - sms_div.append($('
').attr({'id':'sms_code_div'}).html( - '
\ - \ - \ -

')); - + sms_div.append(prompt) $('#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( - '

SMS sent.
Enter verification code received


' - ); - } else { - $('#sms_div').empty().append( - '

SMS not sent


' - ); - } - } - }); - }) } else { - direction = $('
').attr('id','qr_info').text(prompt || 'SMS not sent'); + direction = $('
').attr('id','qr_info').text(prompt || 'SMS was not sent. Please contact Administrator.'); sms_div.append(direction); $('#otp_div').prepend(sms_div) } @@ -331,22 +299,10 @@ var continue_email = function(setup, prompt){ var email_div = $('
').attr({'id':'email_div','style':'padding-bottom:15px;text-align:center;'}); if (setup){ - email_div.append('

Verification code email will be sent to registered email address. Enter code received below

') + email_div.append(prompt) $('#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( - '

Email not sent


' - ); - } - } - }); } else { - var direction = $('
').attr('id','qr_info').text(prompt || 'Verification code email not sent'); + var direction = $('
').attr('id','qr_info').text(prompt || 'Verification code email not sent. Please contact Administrator.'); email_div.append(direction); $('#otp_div').prepend(email_div); } diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 3bcf8c5ce1..1c6b9c1053 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -130,12 +130,13 @@ def get_verification_obj(user,token,otp_secret): if verification_method == 'SMS': verification_obj = process_2fa_for_sms(user,token,otp_secret) elif verification_method == 'OTP App': - if should_send_barcode_as_email(): - verification_obj = process_2fa_for_email(user,token,otp_secret,otp_issuer,method='otp_app') + #check if this if the first time that the user is trying to login. If so, send an email + if not frappe.db.get_default(user + '_otplogin'): + verification_obj = process_2fa_for_email(user,token,otp_secret,otp_issuer,method='OTP App') else: verification_obj = process_2fa_for_otp_app(user,otp_secret,otp_issuer) elif verification_method == 'Email': - process_2fa_for_email(user,token,otp_secret,otp_issuer) + verification_obj = process_2fa_for_email(user,token,otp_secret,otp_issuer) return verification_obj @@ -146,7 +147,8 @@ def process_2fa_for_sms(user,token,otp_secret): status = send_token_via_sms(otp_secret,token=token, phone_no=phone) verification_obj = {'token_delivery': status, 'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]), - 'method': 'SMS'} + 'method': 'SMS', + 'setup': status} return verification_obj def process_2fa_for_otp_app(user,otp_secret,otp_issuer): @@ -163,55 +165,50 @@ def process_2fa_for_otp_app(user,otp_secret,otp_issuer): 'setup': otp_setup_completed } return verification_obj -def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='email'): +def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='Email'): '''Process Email method for 2fa.''' + subject = None message = None status = True - if method == 'otp_app' and not frappe.db.get_default(user + '_otplogin'): + prompt = '' + if method == 'OTP App' and not frappe.db.get_default(user + '_otplogin'): + '''Sending one-time email for OTP App''' totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) 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: - status = send_token_via_email(user,token,otp_secret,otp_issuer,subject=subject,message=message) + 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!!' + else: + '''Sending email verification''' + prompt = 'Verification code has been sent to your registered email address.' + status = send_token_via_email(user,token,otp_secret,otp_issuer,subject=subject,message=message) verification_obj = {'token_delivery': status, - 'prompt': status and 'Verification code has been sent to your registered email address', - 'method': 'Email'} + 'prompt': status and prompt, + 'method': 'Email', + 'setup': status} 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_template = 'Verifcation Code from {}'.format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) 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
{{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_template = 'OTP Registration Code from {}'.format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) 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
{{qrcode_link}}' - template = frappe.get_value('System Settings','System Settings','qr_code_email_body') - if not template == '': - body_template = template + body_template = 'Please click on the following link and follow the instructions on the page.
{{qrcode_link}}' body = render_string_template(body_template,kwargs_dict) return body diff --git a/frappe/www/qrcode.html b/frappe/www/qrcode.html index 3edd1f7e18..3b9c81c531 100644 --- a/frappe/www/qrcode.html +++ b/frappe/www/qrcode.html @@ -1,12 +1,31 @@ +{% extends "templates/web.html" %} + +{% block title %}Register OTP Secret{% endblock %} + +{% block page_content %}
-
- 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. - -
-
- -
+ + + + + +
+
+

+ Hi {{qr_code_user.first_name}}, please perform the following actions: +

  • Open your authentication app on your mobile phone, +
  • Scan the QR Code and enter the resulting code displayed +
  • Return to the Verification screen and enter the code displayed by your authentication app +

    +

    Examples of Authentication Apps you can use are Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile. +

    +
  • +
    +
    + +
    +
    -
    \ No newline at end of file +
    +{% endblock %} \ No newline at end of file diff --git a/frappe/www/qrcode.py b/frappe/www/qrcode.py index f87ede7597..636eabac35 100644 --- a/frappe/www/qrcode.py +++ b/frappe/www/qrcode.py @@ -14,10 +14,6 @@ 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 From 8265acfa1df003293ea2e07b9a46072fb22d47fc Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Mon, 31 Jul 2017 01:16:40 +0100 Subject: [PATCH 26/67] Correcting issue with System Settings json file --- frappe/core/doctype/system_settings/system_settings.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 3f1ad1de3d..c1932a0ed0 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -842,12 +842,14 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "column_break_13", - "fieldtype": "Column Break", + "default": "0", + "description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.", + "fieldname": "enable_password_policy", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, - "in_filter": 0, + "in_filter": 0, "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, From 08743b8100a2fcba8f0e4bd1aadb1435225ff615 Mon Sep 17 00:00:00 2001 From: ckosiegbu Date: Mon, 31 Jul 2017 02:06:23 +0100 Subject: [PATCH 27/67] Changes to OTP settings in System Settings --- .../system_settings/system_settings.json | 96 +++++++------------ frappe/twofactor.py | 11 +-- 2 files changed, 35 insertions(+), 72 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index c1932a0ed0..e1bdaacd0b 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -714,7 +714,8 @@ "collapsible": 0, "columns": 0, "default": "0", - "fieldname": "enable_two_factor_auth", + "description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.", + "fieldname": "enable_password_policy", "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, @@ -723,7 +724,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Enable Two Factor Authentication", + "label": "Enable Password Policy", "length": 0, "no_copy": 0, "permlevel": 0, @@ -744,10 +745,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "OTP App", - "depends_on": "eval:doc.enable_two_factor_auth==1", - "description": "Choose authentication method to be used by all users", - "fieldname": "two_factor_method", + "default": "2", + "depends_on": "eval:doc.enable_password_policy==1", + "fieldname": "minimum_password_score", "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, @@ -756,10 +756,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Authentication method", + "label": "Minimum Password Score", "length": 0, "no_copy": 0, - "options": "OTP App\nSMS\nEmail", + "options": "2\n4", "permlevel": 0, "precision": "", "print_hide": 0, @@ -778,10 +778,9 @@ "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", - "description": "Time in seconds to retain QR code image on server. Min:240", - "fieldname": "lifespan_barcode_image", - "fieldtype": "Int", + "default": "0", + "fieldname": "enable_two_factor_auth", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -789,7 +788,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Delete QR Code Image On server", + "label": "Enable Two Factor Authentication", "length": 0, "no_copy": 0, "permlevel": 0, @@ -810,9 +809,11 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "Frappe Framework", - "fieldname": "otp_issuer_name", - "fieldtype": "Data", + "default": "OTP App", + "depends_on": "eval:doc.enable_two_factor_auth==1", + "description": "Choose authentication method to be used by all users", + "fieldname": "two_factor_method", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -820,10 +821,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "OTP Issuer Name", + "label": "Authentication method", "length": 0, "no_copy": 0, - "options": "", + "options": "OTP App\nSMS\nEmail", "permlevel": 0, "precision": "", "print_hide": 0, @@ -842,18 +843,18 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "0", - "description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.", - "fieldname": "enable_password_policy", - "fieldtype": "Check", + "depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"OTP App\"", + "description": "Time in seconds to retain QR code image on server. Min:240", + "fieldname": "lifespan_qrcode_image", + "fieldtype": "Int", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, - "in_filter": 0, + "in_filter": 0, "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Enable Password Policy", + "label": "Delete QR Code Image On server", "length": 0, "no_copy": 0, "permlevel": 0, @@ -874,10 +875,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "2", - "depends_on": "eval:doc.enable_password_policy==1", - "fieldname": "minimum_password_score", - "fieldtype": "Select", + "default": "Frappe Framework", + "depends_on": "eval:doc.enable_two_factor_auth==1", + "fieldname": "otp_issuer_name", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -885,10 +886,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Minimum Password Score", + "label": "OTP Issuer Name", "length": 0, "no_copy": 0, - "options": "2\n4", + "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1024,37 +1025,6 @@ "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\"", - "fieldname": "send_barcode_as_email", - "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": "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, @@ -1187,8 +1157,8 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-20 22:57:56.466867", - "modified_by": "Administrator", + "modified": "2017-07-31 02:05:48.674604", + "modified_by": "chude.osiegbu@manqala.com", "module": "Core", "name": "System Settings", "name_case": "", diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 1c6b9c1053..ea4d418d0b 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -223,7 +223,7 @@ def get_link_for_qrcode(user,totp_uri): 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')) + lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')) if lifespan<=0: lifespan = 240 frappe.cache().set_value(key_uri,totp_uri,expires_in_sec=lifespan) @@ -276,13 +276,6 @@ def send_token_via_email(user, token, otp_secret, otp_issuer,subject=None,messag enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) return True -def should_send_barcode_as_email(): - settings = frappe.get_doc('System Settings', 'System Settings') - if settings.two_factor_method and settings.send_barcode_as_email: - return True - return False - - def get_qr_svg_code(totp_uri): '''Get SVG code to display Qrcode for OTP.''' url = qrcreate(totp_uri) @@ -344,7 +337,7 @@ def should_remove_barcode_image(barcode): '''Check if it's time to delete barcode image from server. ''' if isinstance(barcode, basestring): barcode = frappe.get_doc('File',barcode) - lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_barcode_image') + lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') if time_diff_in_seconds(get_datetime(),barcode.creation) > int(lifespan): return True return False From 746c2d6ac8de17becc36a5f5264af483c6dd3bdf Mon Sep 17 00:00:00 2001 From: B H Boma Date: Tue, 1 Aug 2017 14:24:42 +0100 Subject: [PATCH 28/67] Tests for twofactor --- frappe/tests/test_twofactor.py | 12 ++++++++++-- frappe/twofactor.py | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index 4dac0774c2..78b56b18d5 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt from __future__ import unicode_literals @@ -50,7 +50,7 @@ class TestTwoFactor(unittest.TestCase): '{} not available'.format(k)) def test_two_factor_is_enabled_for_user(self): - '''Should be true if enabled for user.''' + '''Should return true if enabled for user.''' toggle_2fa_all_role(state=True) self.assertTrue(two_factor_is_enabled_for_(self.user)) toggle_2fa_all_role(state=False) @@ -82,6 +82,13 @@ class TestTwoFactor(unittest.TestCase): token = int(pyotp.TOTP(otp_secret).now()) self.assertTrue(get_verification_obj(self.user,token,otp_secret)) + def test_render_string_template(self): + '''String template renders as expected with variables.''' + args = {'issuer_name':'Frappe Technologies'} + _str = 'Verification Code from {{issuer_name}}' + _str = render_string_template(_str,args) + self.assertEqual(_str,'Verification Code from Frappe Technologies') + def set_request(**kwargs): builder = EnvironBuilder(**kwargs) @@ -106,6 +113,7 @@ def enable_2fa(): frappe.db.commit() def toggle_2fa_all_role(state=None): + '''Enable or disable 2fa for 'all' role on the system.''' all_role = frappe.get_doc('Role','All') if state == None: state = False if all_role.two_factor_auth == True else False diff --git a/frappe/twofactor.py b/frappe/twofactor.py index ea4d418d0b..056ac0388a 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt from __future__ import unicode_literals @@ -190,7 +190,7 @@ def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='Email'): def get_email_subject_for_2fa(kwargs_dict): '''Get email subject for 2fa.''' - subject_template = 'Verifcation Code from {}'.format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) + subject_template = 'Verification Code from {}'.format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) subject = render_string_template(subject_template,kwargs_dict) return subject From 8f68d252f015e93f3d23f1f18d4dd4283a9c2eb2 Mon Sep 17 00:00:00 2001 From: B H Boma Date: Tue, 1 Aug 2017 18:26:17 +0100 Subject: [PATCH 29/67] [fix]Email being sent after each failed login --- frappe/twofactor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 056ac0388a..bcdabc137b 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -38,6 +38,7 @@ def get_cached_user_pass(): def authenticate_for_2factor(user): '''Authenticate two factor for enabled user before login.''' + if frappe.form_dict.get('otp'):return otp_secret = get_otpsecret_for_(user) verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') token = int(pyotp.TOTP(otp_secret).now()) From 8aeeca6b428cfe2a09100fbea6354c40e4f14584 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 16:49:40 +0530 Subject: [PATCH 30/67] [minor] prompt if user has unsaved documents --- frappe/public/js/frappe/desk.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 2816c755ad..50c0c57634 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -45,6 +45,7 @@ frappe.Application = Class.extend({ this.make_nav_bar(); this.set_favicon(); this.setup_analytics(); + this.setup_beforeunload(); frappe.ui.keys.setup(); this.set_rtl(); @@ -480,6 +481,19 @@ frappe.Application = Class.extend({ } }, + setup_beforeunload: function() { + window.onbeforeunload = function () { + var unsaved_docs = []; + for (doctype in locals) { + for (name in locals[doctype]) { + var doc = locals[doctype][name]; + if(doc.__unsaved) { unsaved_docs.push(doc.name); } + } + } + return unsaved_docs.length ? true : false; + }; + }, + show_notes: function() { var me = this; if(frappe.boot.notes.length) { From 9d9cef9c0bab446ff2c87b84e3b2cd10cb3a267f Mon Sep 17 00:00:00 2001 From: Vishal Dhayagude Date: Thu, 3 Aug 2017 16:52:18 +0530 Subject: [PATCH 31/67] [fix] multiple grid_row fetch (#3856) --- frappe/tests/ui/data/test_lib.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/tests/ui/data/test_lib.js b/frappe/tests/ui/data/test_lib.js index ec07cb52ba..772be7a3bf 100644 --- a/frappe/tests/ui/data/test_lib.js +++ b/frappe/tests/ui/data/test_lib.js @@ -54,14 +54,17 @@ frappe.tests = { // build tasks for each row value.forEach(d => { grid_row_tasks.push(() => { - grid.add_new_row(); - let grid_row = grid.get_row(-1).toggle_view(true); + let grid_value_tasks = []; + grid_value_tasks.push(() => grid.add_new_row()); + grid_value_tasks.push(() => grid.get_row(-1).toggle_view(true)); + grid_value_tasks.push(() => frappe.timeout(0.5)); // build tasks to set each row value d.forEach(child_value => { for (let child_key in child_value) { grid_value_tasks.push(() => { + let grid_row = grid.get_row(-1); return frappe.model.set_value(grid_row.doc.doctype, grid_row.doc.name, child_key, child_value[child_key]); }); From e504671de8470cb0580a9e5a5b48ae8981d30479 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 3 Aug 2017 17:01:16 +0530 Subject: [PATCH 32/67] Minor fix in client.get_value (#3852) --- frappe/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/client.py b/frappe/client.py index bc51d4c54f..7d9eb7bbf5 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -69,7 +69,6 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False): fieldname = json.loads(fieldname) except (TypeError, ValueError): # name passed, not json - fieldname = "name" pass # check whether the used filters were really parseable and usable From 6dde90571dc5e932dc333c5a2ef3aa3be50b50c0 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 17:01:01 +0530 Subject: [PATCH 33/67] [test] test_customize_form.js --- .../doctype/customize_form/test_customize_form.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/custom/doctype/customize_form/test_customize_form.js b/frappe/custom/doctype/customize_form/test_customize_form.js index cac3cc15e6..5489dc7f60 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.js +++ b/frappe/custom/doctype/customize_form/test_customize_form.js @@ -7,11 +7,12 @@ QUnit.test("test customize form", function(assert) { let done = assert.async(); frappe.run_serially([ () => frappe.set_route('Form', 'Customize Form'), - () => frappe.timeout(2), + () => frappe.timeout(3), () => cur_frm.set_value('doc_type', 'ToDo'), - () => frappe.timeout(2), + () => frappe.timeout(3), - () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', "Status Field"), + () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', + 'check if second field is "status"'), // open "status" row () => cur_frm.fields_dict.fields.grid.grid_rows[1].toggle_view(), @@ -25,7 +26,8 @@ QUnit.test("test customize form", function(assert) { () => frappe.timeout(0.5), // status still exists - () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', "Status Field Still Exists"), + () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', + 'check if second field is still "status"'), () => done() ]); }); From 4e9512291835d8812717d79f18c3e1abdd456a92 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 17:17:48 +0530 Subject: [PATCH 34/67] [tests] dont show alert if in test --- frappe/core/doctype/test_runner/test_runner.js | 1 + frappe/public/js/frappe/desk.js | 1 + 2 files changed, 2 insertions(+) diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js index 87ea09fab7..da28ab5a2b 100644 --- a/frappe/core/doctype/test_runner/test_runner.js +++ b/frappe/core/doctype/test_runner/test_runner.js @@ -23,6 +23,7 @@ frappe.ui.form.on('Test Runner', { }, run_tests: function(frm, files) { + frappe.flags.in_test = true; let require_list = [ "assets/frappe/js/lib/jquery/qunit.js", "assets/frappe/js/lib/jquery/qunit.css" diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 50c0c57634..9168273e64 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -483,6 +483,7 @@ frappe.Application = Class.extend({ setup_beforeunload: function() { window.onbeforeunload = function () { + if (frappe.flags.in_test) return false; var unsaved_docs = []; for (doctype in locals) { for (name in locals[doctype]) { From 784603acf16a1e3f7d4ae93ad14f7bd2862c3dae Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 17:55:34 +0530 Subject: [PATCH 35/67] [fix] selenium --- frappe/public/js/frappe/desk.js | 3 +++ frappe/tests/ui/test_test_runner.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 9168273e64..849cbff3cf 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -482,6 +482,9 @@ frappe.Application = Class.extend({ }, setup_beforeunload: function() { + if (frappe.defaults.get_default('in_selenium')) { + return; + } window.onbeforeunload = function () { if (frappe.flags.in_test) return false; var unsaved_docs = []; diff --git a/frappe/tests/ui/test_test_runner.py b/frappe/tests/ui/test_test_runner.py index 8b396b6b95..fec5a20d82 100644 --- a/frappe/tests/ui/test_test_runner.py +++ b/frappe/tests/ui/test_test_runner.py @@ -6,6 +6,7 @@ class TestTestRunner(unittest.TestCase): def test_test_runner(self): driver = TestDriver() driver.login() + frappe.db.set_default('in_selenium', '1') for test in get_tests(): if test.startswith('#'): continue @@ -33,6 +34,7 @@ class TestTestRunner(unittest.TestCase): print('Checking if passed "{0}"'.format(test)) self.assertTrue('Tests Passed' in console) time.sleep(1) + frappe.db.set_default('in_selenium', None) driver.close() def get_tests(): From 327c2660d02090f71ac8f3fdce322af6a2890c78 Mon Sep 17 00:00:00 2001 From: Ashwini Save Date: Thu, 3 Aug 2017 18:01:39 +0530 Subject: [PATCH 36/67] Title display issue in mobile UI #3799 (#3850) --- frappe/public/css/mobile.css | 5 ++++- frappe/public/css/page.css | 2 +- frappe/public/less/mobile.less | 5 ++++- frappe/public/less/page.less | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css index ebcc52084f..9bb3e86321 100644 --- a/frappe/public/css/mobile.css +++ b/frappe/public/css/mobile.css @@ -25,6 +25,9 @@ body { body[data-route^="Form"] .page-title h1 { margin-top: 12px; } + body[data-route^="Form"] .page-title h1.editable-title { + padding-right: 80px; + } body[data-route^="Form"] .page-title .indicator { display: inline-block; margin-top: 12px; @@ -197,7 +200,7 @@ body { } body[data-route^="Form"] .page-title .title-text { font-size: 16px; - width: calc(100% - 30px); + width: calc(100% - 90px); } body[data-route^="Form"] .page-title .indicator { float: left; diff --git a/frappe/public/css/page.css b/frappe/public/css/page.css index f5ccdc5a6a..66a7bbd836 100644 --- a/frappe/public/css/page.css +++ b/frappe/public/css/page.css @@ -44,7 +44,6 @@ vertical-align: middle; } .page-title .title-image { - display: inline-block; width: 46px; height: 0; padding: 23px 0; @@ -56,6 +55,7 @@ text-align: center; line-height: 0; float: left; + margin-right: 10px; } .editable-title .title-text { cursor: pointer; diff --git a/frappe/public/less/mobile.less b/frappe/public/less/mobile.less index 40d673c169..ec8bd1df88 100644 --- a/frappe/public/less/mobile.less +++ b/frappe/public/less/mobile.less @@ -34,6 +34,9 @@ body { body[data-route^="Form"] { .page-title h1 { margin-top: 12px; + &.editable-title { + padding-right: 80px; + } } .page-title .indicator { @@ -230,7 +233,7 @@ body { .page-title { .title-text { font-size: 16px; - width: calc(~"100% - 30px"); + width: calc(~"100% - 90px"); } .indicator { float: left; diff --git a/frappe/public/less/page.less b/frappe/public/less/page.less index d141c5bc13..5dc338d3ec 100644 --- a/frappe/public/less/page.less +++ b/frappe/public/less/page.less @@ -54,7 +54,6 @@ } .title-image { - display: inline-block; width: 46px; height: 0; padding: 23px 0; @@ -66,6 +65,7 @@ text-align: center; line-height: 0; float: left; + margin-right: 10px; } } From 8fdc823e7e11ccffa0eba5400eaa40d7188860ce Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 18:10:56 +0530 Subject: [PATCH 37/67] [test] test_customize_form.js --- frappe/custom/doctype/customize_form/test_customize_form.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/custom/doctype/customize_form/test_customize_form.js b/frappe/custom/doctype/customize_form/test_customize_form.js index 5489dc7f60..676be6b3b6 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.js +++ b/frappe/custom/doctype/customize_form/test_customize_form.js @@ -7,9 +7,9 @@ QUnit.test("test customize form", function(assert) { let done = assert.async(); frappe.run_serially([ () => frappe.set_route('Form', 'Customize Form'), - () => frappe.timeout(3), + () => frappe.timeout(5), () => cur_frm.set_value('doc_type', 'ToDo'), - () => frappe.timeout(3), + () => frappe.timeout(2), () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', 'check if second field is "status"'), From 7f4bdc45bc11319cad9957789c54a24dcb1a2f9e Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 18:37:35 +0530 Subject: [PATCH 38/67] [test] test_customize_form.js --- .../custom/doctype/customize_form/test_customize_form.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/custom/doctype/customize_form/test_customize_form.js b/frappe/custom/doctype/customize_form/test_customize_form.js index 676be6b3b6..94f62ab1fb 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.js +++ b/frappe/custom/doctype/customize_form/test_customize_form.js @@ -7,10 +7,12 @@ QUnit.test("test customize form", function(assert) { let done = assert.async(); frappe.run_serially([ () => frappe.set_route('Form', 'Customize Form'), - () => frappe.timeout(5), + () => frappe.timeout(1), () => cur_frm.set_value('doc_type', 'ToDo'), - () => frappe.timeout(2), - + () => frappe.timeout(1), + () => frappe.click_button('Reset to defaults'), + () => frappe.click_button('Submit'), + () => frappe.timeout(1), () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', 'check if second field is "status"'), From 805633860f309a7fa7f2bb6df2b7da189e11a6fb Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 3 Aug 2017 22:22:03 +0530 Subject: [PATCH 39/67] [test] test_customize_form.js --- .../customize_form/test_customize_form.js | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/frappe/custom/doctype/customize_form/test_customize_form.js b/frappe/custom/doctype/customize_form/test_customize_form.js index 94f62ab1fb..d37afa5580 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.js +++ b/frappe/custom/doctype/customize_form/test_customize_form.js @@ -9,15 +9,24 @@ QUnit.test("test customize form", function(assert) { () => frappe.set_route('Form', 'Customize Form'), () => frappe.timeout(1), () => cur_frm.set_value('doc_type', 'ToDo'), - () => frappe.timeout(1), - () => frappe.click_button('Reset to defaults'), - () => frappe.click_button('Submit'), - () => frappe.timeout(1), - () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', - 'check if second field is "status"'), - + () => frappe.timeout(2), + () => { + // find the status column as there may be other custom fields like + // kanban etc. + frappe.row_idx = 0; + cur_frm.doc.fields.every((d, i) => { + if(d.fieldname==='status') { + frappe.row_idx = i; + return false; + } else { + return true; + } + }); + assert.equal(cur_frm.doc.fields[frappe.row_idx].fieldname, 'status', + 'check if selected field is "status"'); + }, // open "status" row - () => cur_frm.fields_dict.fields.grid.grid_rows[1].toggle_view(), + () => cur_frm.fields_dict.fields.grid.grid_rows[frappe.row_idx].toggle_view(), () => frappe.timeout(0.5), // try deleting it @@ -28,8 +37,8 @@ QUnit.test("test customize form", function(assert) { () => frappe.timeout(0.5), // status still exists - () => assert.equal(cur_frm.doc.fields[1].fieldname, 'status', - 'check if second field is still "status"'), + () => assert.equal(cur_frm.doc.fields[frappe.row_idx].fieldname, 'status', + 'check if selected field is still "status"'), () => done() ]); }); From aac8822d052062d2dde137d46033a7dae3e50207 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Fri, 4 Aug 2017 10:53:21 +0530 Subject: [PATCH 40/67] Replaced cPickle import with six.moves.cPickle (#3858) --- frappe/utils/redis_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index fe90f561f6..4feafda718 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import redis, frappe, re -import cPickle as pickle +from six.moves import cPickle as pickle from frappe.utils import cstr from six import iteritems From 913a60dead63bf6d7c954e00888babf7561056ad Mon Sep 17 00:00:00 2001 From: Manas Solanki Date: Fri, 4 Aug 2017 10:53:43 +0530 Subject: [PATCH 41/67] add new argument for transfering data to be printed (#3841) --- frappe/desk/query_report.py | 7 +++++-- .../public/js/frappe/views/reports/query_report.js | 12 +++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 8140a0b11e..073576c437 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -77,7 +77,7 @@ def run(report_name, filters=None, user=None): frappe.msgprint(_("Must have report permission to access this report."), raise_exception=True) - columns, result, message, chart = [], [], None, None + columns, result, message, chart, data_to_be_printed = [], [], None, None, None if report.report_type=="Query Report": if not report.query: frappe.msgprint(_("Must specify a Query to run"), raise_exception=True) @@ -99,6 +99,8 @@ def run(report_name, filters=None, user=None): message = res[2] if len(res) > 3: chart = res[3] + if len(res) > 4: + data_to_be_printed = res[4] if report.apply_user_permissions and result: result = get_filtered_data(report.ref_doctype, columns, result, user) @@ -110,7 +112,8 @@ def run(report_name, filters=None, user=None): "result": result, "columns": columns, "message": message, - "chart": chart + "chart": chart, + "data_to_be_printed": data_to_be_printed } diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 7e0bc14a13..d5bc9a05e5 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -184,12 +184,12 @@ frappe.views.QueryReport = Class.extend({ frappe.msgprint(__("You are not allowed to print this report")); return false; } - if(this.html_format) { var content = frappe.render(this.html_format, { data: frappe.slickgrid_tools.get_filtered_items(this.dataView), filters: this.get_values(), - report: this + report: this, + data_to_be_printed: this.data_to_be_printed }); frappe.render_grid({ @@ -223,7 +223,8 @@ frappe.views.QueryReport = Class.extend({ var content = frappe.render(this.html_format, { data: frappe.slickgrid_tools.get_filtered_items(this.dataView), filters:this.get_values(), - report:this + report:this, + data_to_be_printed: this.data_to_be_printed }); //Render Report in HTML @@ -477,6 +478,7 @@ frappe.views.QueryReport = Class.extend({ this.set_message(res.message); this.setup_chart(res); + this.set_print_data(res.data_to_be_printed); this.toggle_expand_collapse_buttons(this.is_tree_report); }, @@ -887,5 +889,9 @@ frappe.views.QueryReport = Class.extend({ if(this.chart && opts.data && opts.data.rows && opts.data.rows.length) { this.chart_area.toggle(true); } + }, + + set_print_data: function(data_to_be_printed) { + this.data_to_be_printed = data_to_be_printed; } }) From 242bfa421f733e9f49f00c9db7609976e9108a93 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 4 Aug 2017 12:36:28 +0530 Subject: [PATCH 42/67] [minor] clear headline --- frappe/public/js/legacy/form.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/legacy/form.js b/frappe/public/js/legacy/form.js index b431446fd3..3c634dbf2b 100644 --- a/frappe/public/js/legacy/form.js +++ b/frappe/public/js/legacy/form.js @@ -459,6 +459,7 @@ _f.Frm.prototype.refresh = function(docname) { _f.Frm.prototype.show_if_needs_refresh = function() { if(this.doc.__needs_refresh) { if(this.doc.__unsaved) { + this.dashboard.clear_headline(); this.dashboard.set_headline_alert(__("This form has been modified after you have loaded it") + '' + __("Refresh") + '', "alert-warning"); From 644430754e9830a4a56ea47f83c1df46c68baed6 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 4 Aug 2017 15:49:18 +0530 Subject: [PATCH 43/67] [minor] sidebar in website not hidden in mobile --- frappe/public/css/website.css | 27 ++++++++++++++++++++------- frappe/public/less/website.less | 32 +++++++++++++++++++++++++------- frappe/templates/web.html | 3 ++- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index b9b2d733bb..600e8b2fea 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -712,11 +712,6 @@ textarea { .sidebar-navbar-items a:visited { border-bottom: 0px; } -@media (max-width: 767px) { - .visible-xs { - display: inline-block !important; - } -} .more-block { padding-bottom: 30px; } @@ -795,11 +790,29 @@ a.active { padding-top: 30px; padding-bottom: 50px; } +.page-content.with-sidebar { + padding-left: 50px; +} .your-account-info { margin-top: 30px; } -.page-content.with-sidebar { - padding-left: 50px; +@media (max-width: 767px) { + .visible-xs { + display: inline-block !important; + } + .sidebar-block { + width: 100%; + } + .sidebar-block .web-sidebar { + margin: 0px -15px; + padding: 0px 30px; + border-bottom: 1px solid #d1d8dd; + } + .page-content.with-sidebar { + width: 100%; + padding-left: 20px; + padding-right: 20px; + } } @media screen and (max-width: 480px) { .page-content { diff --git a/frappe/public/less/website.less b/frappe/public/less/website.less index 6e9e7916a1..750d7cc33b 100644 --- a/frappe/public/less/website.less +++ b/frappe/public/less/website.less @@ -378,11 +378,6 @@ textarea { } } -@media (max-width: 767px) { - .visible-xs { - display: inline-block !important; - } -} .more-block { padding-bottom: 30px; @@ -480,13 +475,36 @@ a.active { .sidebar-block, .page-content { padding-top: 30px; padding-bottom: 50px; + //width: 20%; } + +.page-content.with-sidebar { + padding-left: 50px; + //width: 80%; +} + .your-account-info { margin-top: 30px; } -.page-content.with-sidebar { - padding-left: 50px; +@media (max-width: 767px) { + .visible-xs { + display: inline-block !important; + } + .sidebar-block { + width: 100%; + + .web-sidebar { + margin: 0px -15px; + padding: 0px 30px; + border-bottom: 1px solid @border-color; + } + } + .page-content.with-sidebar { + width: 100%; + padding-left: 20px; + padding-right: 20px; + } } @media screen and (max-width: 480px) { diff --git a/frappe/templates/web.html b/frappe/templates/web.html index 3f0fc56a85..50f04f95a1 100644 --- a/frappe/templates/web.html +++ b/frappe/templates/web.html @@ -8,7 +8,7 @@ data-doctype="{{ doctype }}"{% endif %}>
    {% if show_sidebar %} -
    +
    From 26a75925ea9e7215257e408b88f7830d2eb32ff1 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 4 Aug 2017 16:57:20 +0530 Subject: [PATCH 44/67] [minor] indicator in form.js --- frappe/public/js/legacy/form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/legacy/form.js b/frappe/public/js/legacy/form.js index 3c634dbf2b..70a9268ea3 100644 --- a/frappe/public/js/legacy/form.js +++ b/frappe/public/js/legacy/form.js @@ -335,7 +335,7 @@ _f.Frm.prototype.refresh_header = function(is_a_different_doc) { ! this.is_dirty() && ! this.is_new() && this.doc.docstatus===0) { - this.dashboard.add_comment(__('Submit this document to confirm'), 'alert-warning', true); + this.dashboard.add_comment(__('Submit this document to confirm'), 'orange', true); } this.clear_custom_buttons(); From 64048e14ac971fd23d2926ef8380c02105787f33 Mon Sep 17 00:00:00 2001 From: B H Boma Date: Fri, 4 Aug 2017 15:59:29 +0100 Subject: [PATCH 45/67] Enable 2fa in sites config instead of system settings, also enabled for admin --- .../system_settings/system_settings.json | 45 +++---------------- frappe/desk/page/setup_wizard/setup_wizard.py | 8 ++-- frappe/tests/test_twofactor.py | 2 +- frappe/twofactor.py | 25 ++++++++--- 4 files changed, 31 insertions(+), 49 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index e1bdaacd0b..3305d2c0e2 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -772,37 +772,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "enable_two_factor_auth", - "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": "Enable 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, @@ -810,7 +779,7 @@ "collapsible": 0, "columns": 0, "default": "OTP App", - "depends_on": "eval:doc.enable_two_factor_auth==1", + "depends_on": "", "description": "Choose authentication method to be used by all users", "fieldname": "two_factor_method", "fieldtype": "Select", @@ -821,7 +790,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Authentication method", + "label": "Two Factor Authentication method", "length": 0, "no_copy": 0, "options": "OTP App\nSMS\nEmail", @@ -843,7 +812,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method == \"OTP App\"", + "depends_on": "eval:doc.two_factor_method == \"OTP App\"", "description": "Time in seconds to retain QR code image on server. Min:240", "fieldname": "lifespan_qrcode_image", "fieldtype": "Int", @@ -854,7 +823,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Delete QR Code Image On server", + "label": "Expiry time of QR Code Image Page", "length": 0, "no_copy": 0, "permlevel": 0, @@ -876,7 +845,7 @@ "collapsible": 0, "columns": 0, "default": "Frappe Framework", - "depends_on": "eval:doc.enable_two_factor_auth==1", + "depends_on": "", "fieldname": "otp_issuer_name", "fieldtype": "Data", "hidden": 0, @@ -1157,8 +1126,8 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-31 02:05:48.674604", - "modified_by": "chude.osiegbu@manqala.com", + "modified": "2017-08-04 12:05:08.054099", + "modified_by": "Administrator", "module": "Core", "name": "System Settings", "name_case": "", diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index d64c8d5601..1b8252477f 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -9,6 +9,7 @@ from frappe.translate import (set_default_language, get_dict, send_translations) from frappe.geo.country_info import get_country_info from frappe.utils.file_manager import save_file from frappe.utils.password import update_password +from frappe.twofactor import toggle_two_factor_auth from werkzeug.useragents import UserAgent import install_fixtures @@ -76,12 +77,11 @@ def update_system_settings(args): 'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"), 'number_format': number_format, 'enable_scheduler': 1 if not frappe.flags.in_test else 0, - 'backup_limit': 3, # Default for downloadable backups - 'enable_two_factor_auth':args.get("twofactor_enable"), - 'two_factor_method':args.get('twofactor_method') + 'backup_limit': 3 # Default for downloadable backups }) if args.get("twofactor_enable") == 1: - enable_twofactor_all_roles() + toggle_two_factor_auth(True,roles=['All']) + system_settings.two_factor_method = args.get('twofactor_method') system_settings.save() def update_user_name(args): diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index 78b56b18d5..900e617360 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -106,8 +106,8 @@ def create_http_request(): def enable_2fa(): '''Enable Two factor in system settings.''' + toggle_two_factor_auth(True) system_settings = frappe.get_doc('System Settings') - system_settings.enable_two_factor_auth = True system_settings.two_factor_method = 'OTP App' system_settings.save(ignore_permissions=True) frappe.db.commit() diff --git a/frappe/twofactor.py b/frappe/twofactor.py index bcdabc137b..44e3fb24df 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -12,18 +12,31 @@ from pyqrcode import create as qrcreate from StringIO import StringIO from base64 import b64encode,b32encode from frappe.utils import get_url, get_datetime, time_diff_in_seconds +from frappe.installer import update_site_config class ExpiredLoginException(Exception):pass + +def toggle_two_factor_auth(state,roles=[]): + '''Enable or disable 2FA in site_config and roles''' + update_site_config('enable_two_factor_auth',state) + for role in roles: + role = frappe.get_doc('Role',{'role_name':role}) + role.two_factor_auth = state + role.save(ignore_permissions=True) + + +def two_factor_is_enabled(user=None): + '''Returns True if 2FA is enabled.''' + enabled = frappe.local.conf.get('enable_two_factor_auth',False) + if not user or not enabled: + return enabled + return two_factor_is_enabled_for_(user) + def should_run_2fa(user): '''Check if 2fa should run.''' - site_otp_enabled = frappe.db.get_value('System Settings', 'System Settings', 'enable_two_factor_auth') - user_otp_enabled = two_factor_is_enabled_for_(user) - #Don't validate for Admin or if not enabled - if user =='Administrator' or not site_otp_enabled or not user_otp_enabled: - return False - return True + return two_factor_is_enabled(user=user) def get_cached_user_pass(): From 343b4cfccdbac25c4b585588addb9cf3ae5ded4e Mon Sep 17 00:00:00 2001 From: Ashwini Save Date: Mon, 7 Aug 2017 13:34:50 +0530 Subject: [PATCH 46/67] Change icons in timeline to Text-All-Format #10273 (#3859) * Change icons in timeline to Text-All-Format #10273 * Changes for delete icon and alignment fixes. --- frappe/public/css/form.css | 27 +++++++++++++++---- frappe/public/css/mobile.css | 11 +++++++- .../public/js/frappe/form/footer/timeline.js | 9 ++++--- .../js/frappe/form/footer/timeline_item.html | 2 +- frappe/public/less/form.less | 27 +++++++++++++++---- frappe/public/less/mobile.less | 11 +++++++- 6 files changed, 70 insertions(+), 17 deletions(-) diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index c56811e892..2dd5aaa1e2 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -299,17 +299,32 @@ h6.uppercase, .timeline-item.user-content .action-btns { position: absolute; right: 0; - padding: 5px 15px 2px 5px; + padding: 8px 15px 0 5px; +} +.timeline-item.user-content .action-btns .edit-btn-container { + margin-right: 13px; } .timeline-item.user-content .comment-header { background-color: #fafbfc; - padding: 10px 15px 10px 13px; + padding: 10px 15px 8px 13px; margin: 0px; color: #8D99A6; border-bottom: 1px solid #EBEFF2; } .timeline-item.user-content .comment-header.links-active { - padding-right: 60px; + padding-right: 77px; +} +.timeline-item.user-content .comment-header .asset-details { + display: inline-block; + width: 100%; +} +.timeline-item.user-content .comment-header .asset-details .btn-link { + border: 0; + border-radius: 0; + padding: 0; +} +.timeline-item.user-content .comment-header .asset-details .btn-link:hover { + text-decoration: none; } .timeline-item.user-content .comment-header .commented-on-small { display: none; @@ -334,7 +349,8 @@ h6.uppercase, .timeline-item.user-content .close-btn-container .close { color: inherit; opacity: 1; - padding: 0 0 0 10px; + padding: 0; + font-size: 18px; } .timeline-item.user-content .edit-btn-container { padding: 0; @@ -409,7 +425,8 @@ h6.uppercase, top: 5px; } .timeline-item .reply-link { - padding-left: 7px; + margin-left: 15px; + font-size: 12px; } .timeline-head { background-color: white; diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css index 9bb3e86321..cc5b926f13 100644 --- a/frappe/public/css/mobile.css +++ b/frappe/public/css/mobile.css @@ -359,7 +359,10 @@ body { content: none; } .timeline .timeline-item.user-content .action-btns { - padding: 5px 10px 2px 5px; + padding: 7px 10px 2px 5px; + } + .timeline .timeline-item.user-content .action-btns .edit-btn-container { + margin-right: 0; } .timeline .timeline-item.user-content .comment-header { padding: 7px 10px; @@ -367,6 +370,12 @@ body { .timeline .timeline-item.user-content .comment-header .links-active { padding-right: 10px; } + .timeline .timeline-item.user-content .comment-header .reply-link { + margin-left: 0; + } + .timeline .timeline-item.user-content .comment-header .asset-details { + width: calc(100% - 30px); + } .timeline .timeline-item.user-content .avatar-medium { margin-right: 10px; } diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 7a48fa2c6f..2937d4f9e7 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -159,12 +159,12 @@ frappe.ui.form.Timeline = Class.extend({ this.prepare_timeline_item(c); var $timeline_item = $(frappe.render_template("timeline_item", {data:c, frm:this.frm})) .appendTo(me.list) - .on("click", ".close", function() { + .on("click", ".delete-comment", function() { var name = $timeline_item.data('name'); me.delete_comment(name); return false; }) - .on('click', '.edit', function(e) { + .on('click', '.edit-comment', function(e) { e.preventDefault(); var name = $timeline_item.data('name'); @@ -176,6 +176,7 @@ frappe.ui.form.Timeline = Class.extend({ var content = $timeline_item.find('.timeline-item-content').html(); $edit_btn + .text("Save") .find('i') .removeClass('octicon-pencil') .addClass('octicon-check'); @@ -251,11 +252,11 @@ frappe.ui.form.Timeline = Class.extend({ c["edit"] = ""; if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment") { if(frappe.model.can_delete("Communication")) { - c["delete"] = ''; + c["delete"] = ''; } if(frappe.user.name == c.sender || (frappe.user.name == 'Administrator')) { - c["edit"] = ''; + c["edit"] = 'Edit'; } } c.comment_on_small = comment_when(c.creation, true); diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html index 215dd06fc2..4baa5504c5 100755 --- a/frappe/public/js/frappe/form/footer/timeline_item.html +++ b/frappe/public/js/frappe/form/footer/timeline_item.html @@ -91,7 +91,7 @@ {% if (data.communication_medium === "Email" && data.sender !== frappe.session.user_email) { %} + data-name="{%= data.name %}" title="{%= __("Reply") %}">{%= __("Reply") %} {% } %} {% } %}