def totp_hook(self, secret=None): nonlocal totp if totp is None: totp = TOTP(secret) if secret: return totp.generate().token else: # on check, take advantage of window because previous token has been # "burned" so we can't generate the same, but tour is so fast # we're pretty certainly within the same 30s return totp.generate(time.time() + 30).token
def verify_totp_code(to_verify: str, my_secret: str, length: int, interval: int, hash_type: str) -> bool: """ Verify given time-based one time password using library passlib :param to_verify: given time-based one time password :type to_verify: str :param my_secret: my local stored secret :type my_secret: str :param length: time-based one time password length Caution Due to a limitation of the HOTP algorithm, the 10th digit can only take on values 0 .. 2, and thus offers very little extra security. Please use maxim lenght of 9 instead 10 limitation see here https://passlib.readthedocs.io/en/stable/lib/passlib.totp.html#totptoken :type length: int :param interval: totp interval in sec :type interval: int :param hash_type: hash code type to be used sha1, sha256 or sha512 :type hash_type: str :return: result of totp matches :rtype: bool """ logger.debug("verify totp with library passlib") totp = TOTP(key=my_secret, digits=length, period=interval, alg=hash_type) if totp.generate().token == str(to_verify): return True else: return False
def generate_totp_code(my_secret: str, length: int, interval: int, hash_type: str) -> bool: """ Generate a new time-based one time password using library passlib :param my_secret: my local stored secret :type my_secret: str :param length: time-based one time password length Caution Due to a limitation of the HOTP algorithm, the 10th digit can only take on values 0 .. 2, and thus offers very little extra security. Please use maxim lenght of 9 instead 10 limitation see here: https://passlib.readthedocs.io/en/stable/lib/passlib.totp.html#totptoken :type length: int :param interval: totp interval in sec :type interval: int :param hash_type: hash code type to be used sha1, sha256 or sha512 :type hash_type: str :return: totp code :rtype: int """ logger.debug("generate new totp code with library passlib") totp = TOTP(key=my_secret, digits=length, period=interval, alg=hash_type) return totp.generate().token
def test_two_factor_flag(app, client): # trying to verify code without going through two-factor # first login function wrong_code = b"000000" response = client.post( "/tf-validate", data=dict(code=wrong_code), follow_redirects=True ) message = b"You currently do not have permissions to access this page" assert message in response.data # Test login using invalid email data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Specified user does not exist" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Specified user does not exist" in response.data # Test login using valid email and invalid password data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Invalid password" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Invalid password" in response.data # Test two-factor authentication first login data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-factor authentication adds an extra layer of security" assert message in response.data response = client.post( "/tf-setup", data=dict(setup="not_a_method"), follow_redirects=True ) assert b"Marked method is not valid" in response.data session = get_session(response) assert session["tf_state"] == "setup_from_login" # try non-existing setup on setup page (using json) data = dict(setup="not_a_method") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert response.status_code == 400 assert ( response.json["response"]["errors"]["setup"][0] == "Marked method is not valid" ) data = dict(setup="email") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Test for sms in process of valid login sms_sender = SmsSenderFactory.createSender("test") data = dict(email="*****@*****.**", password="******") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b'"code": 200' in response.data assert sms_sender.get_count() == 1 session = get_session(response) assert session["tf_state"] == "ready" code = sms_sender.messages[0].split()[-1] # submit bad token to two_factor_token_validation response = client.post("/tf-validate", data=dict(code=wrong_code)) assert b"Invalid Token" in response.data # sumbit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data # Upon completion, session cookie shouldnt have any two factor stuff in it. assert not tf_in_session(get_session(response)) # Test change two_factor view to from sms to mail with app.mail.record_messages() as outbox: setup_data = dict(setup="email") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) msg = b"To complete logging in, please enter the code sent to your mail" assert msg in response.data # Fetch token validate form response = client.get("/tf-validate") assert response.status_code == 200 assert b'name="code"' in response.data code = outbox[0].body.split()[-1] # sumbit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data # Test change two_factor password confirmation view to authenticator # Setup authenticator setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) assert b"Open an authenticator app on your device" in response.data # verify png QRcode is present assert b"data:image/svg+xml;base64," in response.data # parse out key rd = response.data.decode("utf-8") matcher = re.match(r".*((?:\S{4}-){7}\S{4}).*", rd, re.DOTALL) totp_secret = matcher.group(1) # Generate token from passed totp_secret and confirm setup totp = TOTP(totp_secret) code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data logout(client) # Test login with remember_token assert "remember_token" not in [c.name for c in client.cookie_jar] data = dict(email="*****@*****.**", password="******", remember=True) response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Generate token from passed totp_secret code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data # Verify that the remember token is properly set found = False for cookie in client.cookie_jar: if cookie.name == "remember_token": found = True assert cookie.path == "/" assert found response = logout(client) # Verify that logout clears session info assert not tf_in_session(get_session(response)) # Test two-factor authentication first login data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-factor authentication adds an extra layer of security" assert message in response.data # check availability of qrcode when this option is not picked assert b"data:image/png;base64," not in response.data # check availability of qrcode page when this option is picked setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) assert b"Open an authenticator app on your device" in response.data assert b"data:image/svg+xml;base64," in response.data # check appearence of setup page when sms picked and phone number entered sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"To Which Phone Number Should We Send Code To" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your token has been confirmed" in response.data assert not tf_in_session(get_session(response)) logout(client) # check when two_factor_rescue function should not appear rescue_data_json = dict(help_setup="lost_device") response = client.post( "/tf-rescue", json=rescue_data_json, headers={"Content-Type": "application/json"}, ) assert b'"code": 400' in response.data # check when two_factor_rescue function should appear data = dict(email="*****@*****.**", password="******") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data rescue_data = dict(help_setup="lost_device") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"The code for authentication was sent to your email address" assert message in response.data rescue_data = dict(help_setup="no_mail_access") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"A mail was sent to us in order to reset your application account" assert message in response.data