def test_otp_attempt_tracker(self): """Check that OTP login attempts are tracked. """ 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) with self.assertRaises(frappe.AuthenticationError): confirm_otp_token(self.login_manager, otp=otp, tmp_id=tmp_id) # REMOVE ME: current logic allows allow_consecutive_login_attempts+1 attempts # before raising security exception, remove below line when that is fixed. with self.assertRaises(frappe.AuthenticationError): confirm_otp_token(self.login_manager, otp=otp, tmp_id=tmp_id) with self.assertRaises(frappe.SecurityException): confirm_otp_token(self.login_manager, otp=otp, tmp_id=tmp_id) # Remove tracking cache so that user can try loging in again tracker = get_login_attempt_tracker(self.user, raise_locked_exception=False) tracker.add_success_attempt() otp = get_otp(self.user) self.assertTrue( confirm_otp_token(self.login_manager, otp=otp, tmp_id=tmp_id))
def confirm_otp_token(login_manager, otp=None, tmp_id=None): """Confirm otp matches.""" from frappe.auth import get_login_attempt_tracker 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 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: raise ExpiredLoginException( _("Login session expired, refresh page to retry")) tracker = get_login_attempt_tracker(login_manager.user) hotp = pyotp.HOTP(otp_secret) if hotp_token: if hotp.verify(otp, int(hotp_token)): frappe.cache().delete(tmp_id + "_token") tracker.add_success_attempt() return True else: tracker.add_failure_attempt() 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) delete_qrimage(login_manager.user) tracker.add_success_attempt() return True else: tracker.add_failure_attempt() login_manager.fail(_("Incorrect Verification code"), login_manager.user)