From 977130807234d75a32187673254d0ba1e007f033 Mon Sep 17 00:00:00 2001 From: crossxcell99 Date: Wed, 28 Jun 2017 18:02:20 +0100 Subject: [PATCH 01/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] [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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] [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/34] [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/34] 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/34] [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/34] [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/34] 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/34] 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/34] 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/34] 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/34] [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 64048e14ac971fd23d2926ef8380c02105787f33 Mon Sep 17 00:00:00 2001 From: B H Boma Date: Fri, 4 Aug 2017 15:59:29 +0100 Subject: [PATCH 30/34] 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 bc4d46a362497fbf4c021804502cbff7602cc81c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 8 Aug 2017 14:29:22 +0530 Subject: [PATCH 31/34] [fix] style and move setup to system settings --- frappe/auth.py | 26 +-- .../system_settings/system_settings.json | 126 +++++++--- .../system_settings/system_settings.py | 7 + frappe/desk/page/setup_wizard/setup_wizard.py | 2 +- frappe/public/css/website.css | 7 + frappe/public/less/website.less | 6 + frappe/templates/includes/login/login.js | 6 +- frappe/templates/web.html | 2 +- frappe/twofactor.py | 217 +++++++++--------- frappe/website/router.py | 1 - frappe/website/utils.py | 6 +- frappe/www/qrcode.html | 44 ++-- frappe/www/qrcode.py | 4 +- 13 files changed, 269 insertions(+), 185 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 7fb9bb58de..bd510b9fcd 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -17,13 +17,12 @@ 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 should_run_2fa, authenticate_for_2factor, \ - confirm_otp_token,get_cached_user_pass - +from twofactor import (should_run_2fa, authenticate_for_2factor, + confirm_otp_token, get_cached_user_pass) from six.moves.urllib.parse import quote -import pyotp,base64,os +import pyotp, base64, os class HTTPRequest: def __init__(self): @@ -68,7 +67,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: return if not frappe.local.session.data.csrf_token \ or frappe.local.session.data.device=="mobile" \ or frappe.conf.get('ignore_csrf', None): @@ -95,7 +94,7 @@ class HTTPRequest: def connect(self, ac_name = None): """connect to db, from ac_name or db_name""" frappe.local.db = frappe.database.Database(user = self.get_db_name(), \ - password = getattr(conf,'db_password', '')) + password = getattr(conf, 'db_password', '')) class LoginManager: def __init__(self): @@ -105,7 +104,7 @@ class LoginManager: self.user_type = None if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": - if self.login()==False:return + if self.login()==False: return self.resume = False # run login triggers @@ -120,20 +119,17 @@ class LoginManager: self.make_session() self.set_user_info() - def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) - user,pwd = get_cached_user_pass() - self.authenticate(user=user,pwd=pwd) + 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): self.run_trigger('on_login') self.validate_ip_address() @@ -198,7 +194,7 @@ class LoginManager: if not (user and pwd): user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') if not (user and pwd): - self.fail('Incomplete login details', user=user) + self.fail(_('Incomplete login details'), user=user) if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")): user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user @@ -220,7 +216,9 @@ class LoginManager: except frappe.AuthenticationError: self.fail('Incorrect password', user=user) - def fail(self, message, user="NA"): + def fail(self, message, user=None): + if not user: + user = _('Unknown User') frappe.local.response['message'] = message add_authentication_log(message, user, status="Failed") frappe.db.commit() diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 3305d2c0e2..6405a275bf 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -778,11 +778,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "OTP App", - "depends_on": "", - "description": "Choose authentication method to be used by all users", - "fieldname": "two_factor_method", - "fieldtype": "Select", + "fieldname": "column_break_13", + "fieldtype": "Column Break", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -790,10 +787,8 @@ "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, @@ -812,10 +807,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "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", + "description": "Note: Multiple sessions will be allowed in case of mobile device", + "fieldname": "deny_multiple_sessions", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -823,7 +817,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Expiry time of QR Code Image Page", + "label": "Allow only one session per user", "length": 0, "no_copy": 0, "permlevel": 0, @@ -844,10 +838,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "Frappe Framework", - "depends_on": "", - "fieldname": "otp_issuer_name", - "fieldtype": "Data", + "description": "User can login using Email id or Mobile number", + "fieldname": "allow_login_using_mobile_number", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -855,10 +848,9 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "OTP Issuer Name", + "label": "Allow Login using Mobile Number", "length": 0, "no_copy": 0, - "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -877,8 +869,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "column_break_13", - "fieldtype": "Column Break", + "default": "1", + "description": "", + "fieldname": "allow_error_traceback", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -886,6 +880,37 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, + "label": "Show Full Error and Allow Reporting of Issues to the Developer", + "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": 1, + "columns": 0, + "fieldname": "two_factor_authentication", + "fieldtype": "Section 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, + "label": "Two Factor Authentication", "length": 0, "no_copy": 0, "permlevel": 0, @@ -906,8 +931,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "Note: Multiple sessions will be allowed in case of mobile device", - "fieldname": "deny_multiple_sessions", + "fieldname": "enable_two_factor_auth", "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, @@ -916,7 +940,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow only one session per user", + "label": "Enable Two Factor Auth", "length": 0, "no_copy": 0, "permlevel": 0, @@ -937,9 +961,11 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "User can login using Email id or Mobile number", - "fieldname": "allow_login_using_mobile_number", - "fieldtype": "Check", + "default": "OTP App", + "depends_on": "", + "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, @@ -947,9 +973,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow Login using Mobile Number", + "label": "Two Factor Authentication method", "length": 0, "no_copy": 0, + "options": "OTP App\nSMS\nEmail", "permlevel": 0, "precision": "", "print_hide": 0, @@ -968,10 +995,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "1", - "description": "", - "fieldname": "allow_error_traceback", - "fieldtype": "Check", + "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", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -979,7 +1006,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Show Full Error and Allow Reporting of Issues to the Developer", + "label": "Expiry time of QR Code Image Page", "length": 0, "no_copy": 0, "permlevel": 0, @@ -994,6 +1021,39 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Frappe Framework", + "depends_on": "enable_two_factor_auth", + "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, @@ -1126,7 +1186,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-08-04 12:05:08.054099", + "modified": "2017-08-07 23:29:18.858797", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index f7ecfc00bb..cd7edc6a53 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -9,6 +9,7 @@ from frappe.model import no_value_fields from frappe.translate import set_default_language from frappe.utils import cint from frappe.utils.momentjs import get_all_timezones +from frappe.twofactor import toggle_two_factor_auth class SystemSettings(Document): def validate(self): @@ -25,6 +26,12 @@ class SystemSettings(Document): if len(parts)!=2 or not (cint(parts[0]) or cint(parts[1])): frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm")) + if self.enable_two_factor_auth: + if self.two_factor_method=='SMS': + if not frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'): + frappe.throw(_('Please setup SMS before setting it as an authentication method, via SMS Settings')) + toggle_two_factor_auth(True, roles=['All']) + def on_update(self): for df in self.meta.get("fields"): if df.fieldtype not in no_value_fields: diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 1b8252477f..5b6b530dc1 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -80,7 +80,7 @@ def update_system_settings(args): 'backup_limit': 3 # Default for downloadable backups }) if args.get("twofactor_enable") == 1: - toggle_two_factor_auth(True,roles=['All']) + toggle_two_factor_auth(True, roles=['All']) system_settings.two_factor_method = args.get('twofactor_method') system_settings.save() diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index 95bd9edd0e..6e33918c6c 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -507,6 +507,7 @@ li { border-top: 1px solid #EBEFF2; } .page_content { + padding-top: 30px; padding-bottom: 30px; } .carousel-control .icon { @@ -554,6 +555,9 @@ li { .panel-body { padding-left: 15px; } +.page-head { + margin-bottom: -30px; +} .page-head h1, .page-head h2 { margin-top: 0px; @@ -813,6 +817,9 @@ a.active { padding: 30px; padding-left: 40px; } +.page-content.without-sidebar { + padding-top: 30px; +} .your-account-info { margin-top: 30px; } diff --git a/frappe/public/less/website.less b/frappe/public/less/website.less index 189ee6275f..87f5065e66 100644 --- a/frappe/public/less/website.less +++ b/frappe/public/less/website.less @@ -125,6 +125,7 @@ li { } .page_content { + padding-top: 30px; padding-bottom: 30px; } @@ -181,6 +182,7 @@ li { } .page-head { + margin-bottom: -30px; h1, h2 { margin-top: 0px; } @@ -504,6 +506,10 @@ a.active { padding-left: 40px; } +.page-content.without-sidebar { + padding-top: 30px; +} + .your-account-info { margin-top: 30px; } diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index cd65cdb250..69e1199254 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -267,7 +267,7 @@ var request_otp = function(r){ var continue_otp_app = function(setup, qrcode){ request_otp(); - var qrcode_div = $('
    ').attr({'id':'qrcode_div','style':'text-align:center;padding-bottom:15px;'}); + var qrcode_div = $('
    '); if (setup){ direction = $('
    ').attr('id','qr_info').text('Enter Code displayed in OTP App.'); @@ -282,7 +282,7 @@ var continue_otp_app = function(setup, qrcode){ var continue_sms = function(setup, prompt){ request_otp(); - var sms_div = $('
    ').attr({'id':'sms_div','style':'padding-bottom:15px;text-align:center;'}); + var sms_div = $('
    '); if (setup){ sms_div.append(prompt) @@ -296,7 +296,7 @@ var continue_sms = function(setup, prompt){ var continue_email = function(setup, prompt){ request_otp(); - var email_div = $('
    ').attr({'id':'email_div','style':'padding-bottom:15px;text-align:center;'}); + var email_div = $('
    '); if (setup){ email_div.append(prompt) diff --git a/frappe/templates/web.html b/frappe/templates/web.html index cb7a1aa09e..44b5c1cb8c 100644 --- a/frappe/templates/web.html +++ b/frappe/templates/web.html @@ -11,7 +11,7 @@ {% include "templates/includes/web_sidebar.html" %}
    {% endif %} -
    +
    diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 44e3fb24df..3e0fbf69de 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -5,31 +5,27 @@ from __future__ import unicode_literals import frappe from frappe import _ -import pyotp,base64,os +import pyotp, 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 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 -class ExpiredLoginException(Exception):pass - - -def toggle_two_factor_auth(state,roles=[]): +def toggle_two_factor_auth(state, roles=[]): '''Enable or disable 2FA in site_config and roles''' - update_site_config('enable_two_factor_auth',state) + frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 1) for role in roles: - role = frappe.get_doc('Role',{'role_name':role}) + 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) + enabled = frappe.db.get_value('System Settings', None, 'enable_two_factor_auth') if not user or not enabled: return enabled return two_factor_is_enabled_for_(user) @@ -38,7 +34,6 @@ def should_run_2fa(user): '''Check if 2fa should run.''' return two_factor_is_enabled(user=user) - def get_cached_user_pass(): '''Get user and password if set.''' user = pwd = None @@ -46,23 +41,22 @@ def get_cached_user_pass(): if tmp_id: user = frappe.cache().get(tmp_id+'_usr') pwd = frappe.cache().get(tmp_id+'_pwd') - return (user,pwd) - + return (user, pwd) def authenticate_for_2factor(user): '''Authenticate two factor for enabled user before login.''' - if frappe.form_dict.get('otp'):return + 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()) 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) + 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): +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() @@ -74,20 +68,24 @@ def cache_2fa_data(user,token,otp_secret,tmp_id): 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) + 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 + if isinstance(user, basestring): + user = frappe.get_doc('User', user) + + roles = [frappe.db.escape(d.role) for d in user.roles or []] + roles.append('All') + + query = """select name from `tabRole` where two_factor_auth=1 + and name in ({0}) limit 1""".format(', '.join('\"{}\"'.format(i) for \ + i in roles)) + if len(frappe.db.sql(query)) > 0: + return True + return False def get_otpsecret_for_(user): @@ -102,9 +100,7 @@ def get_otpsecret_for_(user): def get_verification_method(): return frappe.db.get_value('System Settings', None, 'two_factor_method') - - -def confirm_otp_token(login_manager,otp=None,tmp_id=None): +def confirm_otp_token(login_manager, otp=None, tmp_id=None): '''Confirm otp matches.''' if not otp: otp = frappe.form_dict.get('otp') @@ -119,7 +115,7 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None): if not otp_secret: raise ExpiredLoginException(_('Login session expired, refresh page to retry')) hotp = pyotp.HOTP(otp_secret) - if hotp_token: + if hotp_token: if hotp.verify(otp, int(hotp_token)): frappe.cache().delete(tmp_id + '_token') return True @@ -137,35 +133,37 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None): login_manager.fail(_('Incorrect Verification code'), login_manager.user) -def get_verification_obj(user,token,otp_secret): +def get_verification_obj(user, token, otp_secret): 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) + verification_obj = process_2fa_for_sms(user, token, otp_secret) elif verification_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') + 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) + verification_obj = process_2fa_for_otp_app(user, otp_secret, otp_issuer) elif verification_method == 'Email': - verification_obj = 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 -def process_2fa_for_sms(user,token,otp_secret): +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 = 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', - 'setup': status} + 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', + 'setup': status + } return verification_obj -def process_2fa_for_otp_app(user,otp_secret,otp_issuer): +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'): @@ -173,13 +171,15 @@ def process_2fa_for_otp_app(user,otp_secret,otp_issuer): 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 } + 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,method='Email'): +def process_2fa_for_email(user, token, otp_secret, otp_issuer, method='Email'): '''Process Email method for 2fa.''' subject = None message = None @@ -188,51 +188,53 @@ 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'): '''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}) - 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!!' + 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}) + 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 prompt, - 'method': 'Email', - 'setup': status} + 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 prompt, + 'method': 'Email', + 'setup': status + } return verification_obj def get_email_subject_for_2fa(kwargs_dict): '''Get email subject for 2fa.''' - 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) + subject_template = _('Login 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 def get_email_body_for_2fa(kwargs_dict): '''Get email body for 2fa.''' - body_template = 'Use this token to login
    {{otp}}' - body = render_string_template(body_template,kwargs_dict) + body_template = 'Enter this code to complete your login:

    {{otp}}' + 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 = 'OTP Registration Code from {}'.format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) - subject = render_string_template(subject_template,kwargs_dict) + subject_template = _('One Time Password (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 = 'Please click on the following link and follow the instructions on the page.
    {{qrcode_link}}' - body = render_string_template(body_template,kwargs_dict) + 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 -def render_string_template(_str,kwargs_dict): +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): +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) @@ -240,8 +242,8 @@ def get_link_for_qrcode(user,totp_uri): 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) - frappe.cache().set_value(key_user,user,expires_in_sec=lifespan) + 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): @@ -258,7 +260,7 @@ def send_token_via_sms(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: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer} for d in ss.get("parameters"): @@ -266,28 +268,35 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None): args[ss.receiver_parameter] = phone_no - sms_args = {'gateway_url':ss.sms_gateway_url,'params':args} + 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,subject=None,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} + template_args = {'otp': otp, 'otp_issuer': otp_issuer} if not subject: subject = get_email_subject_for_2fa(template_args) if not message: message = get_email_body_for_2fa(template_args) - email_args = { - 'recipients':user_email, 'sender':None, 'subject':subject, - '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) + email_args = { + 'recipients': user_email, + 'sender': None, + 'subject': subject, + 'message': message, + 'header': [_('Verfication Code'), 'blue'], + '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): @@ -297,62 +306,62 @@ def get_qr_svg_code(totp_uri): stream = StringIO() try: url.svg(stream, scale=4, background="#eee", module_color="#222") - svg = stream.getvalue().replace('\n','') + svg = stream.getvalue().replace('\n', '') svg = b64encode(bytes(svg)) finally: stream.close() return svg -def qrcode_as_png(user,totp_uri): +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) + 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]) + 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}) + folder = frappe.db.exists('File', {'file_name': folder_name}) if folder: return folder folder = frappe.get_doc({ - 'doctype':'File', - 'file_name':folder_name, + 'doctype': 'File', + 'file_name': folder_name, 'is_folder':1, - 'folder':'Home' + 'folder': 'Home' }) folder.insert(ignore_permissions=True) return folder.name -def delete_qrimage(user,check_expiry=False): +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'}) + 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) + 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}) + users = frappe.get_all('User', {'enabled':1}) for user in users: - delete_qrimage(user.name,check_expiry=True) + 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) + barcode = frappe.get_doc('File', barcode) lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') - if time_diff_in_seconds(get_datetime(),barcode.creation) > int(lifespan): + if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan): return True return False diff --git a/frappe/website/router.py b/frappe/website/router.py index 6bf75c8f46..fa79d807dd 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -35,7 +35,6 @@ def get_page_context(path): page_context = make_page_context(path) if can_cache(page_context.no_cache): page_context_cache[frappe.local.lang] = page_context - frappe.cache().hset("page_context", path, page_context_cache) return page_context diff --git a/frappe/website/utils.py b/frappe/website/utils.py index c4f167f2bc..7a80d03f85 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -24,7 +24,11 @@ def find_first_image(html): return None def can_cache(no_cache=False): - return not (frappe.conf.disable_website_cache or getattr(frappe.local, "no_cache", False) or no_cache) + if frappe.conf.disable_website_cache or frappe.conf.developer_mode: + return False + if getattr(frappe.local, "no_cache", False): + return False + return not no_cache def get_comment_list(doctype, name): return frappe.db.sql("""select diff --git a/frappe/www/qrcode.html b/frappe/www/qrcode.html index 3b9c81c531..4cbedb1060 100644 --- a/frappe/www/qrcode.html +++ b/frappe/www/qrcode.html @@ -1,31 +1,27 @@ {% extends "templates/web.html" %} -{% block title %}Register OTP Secret{% endblock %} +{% block title %}{{ _("QR Code") }}{% endblock %} {% block page_content %} -
    -
    - - - - - -
    -
    -

    - 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. -

    -
  • -
    -
    - -
    -
    +

    {{ _("QR Code for Login Verification") }}

    +
    +
    +

    {{ _("Hi {0}").format(qr_code_user.first_name) }},

    + +

    {{ _("Steps to verify your login") }}:

    +
      +
    1. {{ _("Open your authentication app on your mobile phone.") }} +
    2. {{ _("Scan the QR Code and enter the resulting code displayed.") }} +
    3. {{ _("Return to the Verification screen and enter the code displayed by your authentication app") }} +
    +

    +
    +

    {{ _("Authentication Apps you can use are: ") }} + Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile. +

    +
    +
    +
    {% endblock %} \ No newline at end of file diff --git a/frappe/www/qrcode.py b/frappe/www/qrcode.py index 636eabac35..bf7d79236e 100644 --- a/frappe/www/qrcode.py +++ b/frappe/www/qrcode.py @@ -8,10 +8,8 @@ from frappe import _ from urlparse import parse_qs from frappe.twofactor import get_qr_svg_code -no_cache = 1 - - def get_context(context): + context.no_cache = 1 context.qr_code_user,context.qrcode_svg = get_user_svg_from_cache() def get_query_key(): From e5138af195e3864b2bc121982098e075d3960745 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 8 Aug 2017 14:32:32 +0530 Subject: [PATCH 32/34] [add] disable 2fa --- frappe/twofactor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 3e0fbf69de..cf49055d91 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -17,7 +17,6 @@ class ExpiredLoginException(Exception): pass def toggle_two_factor_auth(state, roles=[]): '''Enable or disable 2FA in site_config and roles''' - frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 1) for role in roles: role = frappe.get_doc('Role', {'role_name': role}) role.two_factor_auth = state @@ -365,3 +364,6 @@ def should_remove_barcode_image(barcode): return True return False +def disable(): + frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 0) + From 925e5922eb4547c76c0dc4bbee0b41e9ddd1132f Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 8 Aug 2017 14:35:07 +0530 Subject: [PATCH 33/34] [minor] cleaned code from user.js --- frappe/core/doctype/user/user.js | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 601c7d27ce..5409b569c7 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -77,7 +77,7 @@ frappe.ui.form.on('User', { } }) }) - + frm.add_custom_button(__("Reset OTP Secret"), function() { frappe.call({ method: "frappe.core.doctype.user.user.reset_otp_secret", @@ -121,27 +121,6 @@ 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) { From a6e1210162681b16d437218b4e9df46143ff0aee Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 8 Aug 2017 14:42:02 +0530 Subject: [PATCH 34/34] [fix] tests --- frappe/tests/test_twofactor.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index 900e617360..e993b2d517 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -5,27 +5,25 @@ from __future__ import unicode_literals import unittest, frappe, pyotp from werkzeug.wrappers import Request from werkzeug.test import EnvironBuilder -from frappe.auth import LoginManager, HTTPRequest -from frappe.twofactor import * -import time - +from frappe.auth import HTTPRequest +from frappe.twofactor import (should_run_2fa, authenticate_for_2factor, get_cached_user_pass, + two_factor_is_enabled_for_, confirm_otp_token, get_otpsecret_for_, get_verification_obj, + render_string_template) +import time 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 def tearDown(self): - tmp_id = frappe.local.response['tmp_id'] frappe.local.response['verification'] = None frappe.local.response['tmp_id'] = None + disable_2fa() frappe.clear_cache(user=self.user) - def test_should_run_2fa(self): '''Should return true if enabled.''' toggle_2fa_all_role(state=True) @@ -68,7 +66,7 @@ class TestTwoFactor(unittest.TestCase): otp = 'wrongotp' with self.assertRaises(frappe.AuthenticationError): confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id) - otp = get_otp(self.user) + 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..') @@ -106,12 +104,18 @@ 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 = 1 system_settings.two_factor_method = 'OTP App' system_settings.save(ignore_permissions=True) frappe.db.commit() +def disable_2fa(): + system_settings = frappe.get_doc('System Settings') + system_settings.enable_two_factor_auth = 0 + system_settings.save(ignore_permissions=True) + 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')