1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
"""
flask_security.totp
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Flask-Security TOTP (Timed-One-Time-Passwords) module
:copyright: (c) 2019-2021 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""
import base64
import io
from passlib.totp import TOTP, TokenError
class Totp:
"""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_totp_pretty_key(self, totp_secret):
"""Generate pretty key for manual input
:param totp_secret: a unique shared secret of the user
.. versionadded:: 4.0.0
"""
tp = self._totp.from_source(totp_secret)
return tp.pretty_key()
def fetch_setup_values(self, totp, user):
"""Generate various values user needs to setup authenticator app.
Returns dict with keys:
'key': totp key
'image': image as string (useful for <img src=xx>)
'username: qrcode best practice
'issuer': qrcode best practice
.. versionadded:: 4.0.0
"""
r = dict()
# By convention, the URI should have the username that the user
# logs in with.
username = user.calc_username() or "Unknown"
r["username"] = username
r["key"] = self.get_totp_pretty_key(totp)
r["issuer"] = self._totp.issuer
r["image"] = self.generate_qrcode(username, totp)
return r
def generate_qrcode(self, username, totp):
"""Generate QRcode
Using username, totp, generate the actual QRcode image.
This method can be overridden to fine-tune how the image is created -
such as size, color etc.
It must return a string suitable for use in an <img src=xx> tag.
.. versionadded:: 4.0.0
"""
try:
import pyqrcode
code = pyqrcode.create(self.get_totp_uri(username, totp))
with io.BytesIO() as virtual_file:
code.svg(file=virtual_file, scale=3)
image_as_str = base64.b64encode(virtual_file.getvalue()).decode("ascii")
return f"data:image/svg+xml;base64,{image_as_str}"
except ImportError: # pragma: no cover
# This should have been checked at app init.
raise
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
|