# -*- coding: utf-8 -*- """ flask_security.totp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security TOTP (Timed-One-Time-Passwords) module :copyright: (c) 2019 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from passlib.totp import TOTP, TokenError class Totp(object): """ Encapsulate usage of Passlib TOTP functionality. Flask-Security doesn't implement any replay-attack protection out of the box as suggested by: https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#match-verify Subclass this and implement the get/set last_counter methods. Your subclass can be registered at Flask-Security creation/initialization time. .. versionadded:: 3.4.0 """ def __init__(self, secrets, issuer): """ Initialize a totp factory. secrets are used to encrypt the per-user totp_secret on disk. """ # This should be a dict with at least one entry if not isinstance(secrets, dict) or len(secrets) < 1: raise ValueError("secrets needs to be a dict with at least one entry") self._totp = TOTP.using(issuer=issuer, secrets=secrets) def generate_totp_password(self, totp_secret): """Get time-based one-time password on the basis of given secret and time :param totp_secret: the unique shared secret of the user """ return self._totp.from_source(totp_secret).generate().token def generate_totp_secret(self): """ Create new user-unique totp_secret. We return an encrypted json string so that when sent in a cookie or sent to DB - it is encrypted. """ return self._totp.new().to_json(encrypt=True) def verify_totp(self, token, totp_secret, user, window=0): """ Verifies token for specific user. :param token: token to be check against user's secret :param totp_secret: the unique shared secret of the user :param user: User model :param window: optional. How far backward and forward in time to search for a match. Measured in seconds. :return: True if match """ # TODO - in old implementation using onetimepass window was described # as 'compensate for clock skew) and 'interval_length' would say how long # the token is good for. # In passlib - 'window' means how far back and forward to look and 'clock_skew' # is specifically for well, clock slew. try: tmatch = self._totp.verify( token, totp_secret, window=window, last_counter=self.get_last_counter(user), ) self.set_last_counter(user, tmatch) return True except TokenError: return False def get_totp_uri(self, username, totp_secret): """ Generate provisioning url for use with the qrcode scanner built into the app :param username: username/email of the current user :param totp_secret: a unique shared secret of the user """ tp = self._totp.from_source(totp_secret) return tp.to_uri(username) def get_last_counter(self, user): """ Implement this to fetch stored last_counter from cache. :param user: User model :return: last_counter as stored in set_last_counter() """ return None def set_last_counter(self, user, tmatch): """ Implement this to cache last_counter. :param user: User model :param tmatch: a TotpMatch as returned from totp.verify() """ pass