File: totp.py

package info (click to toggle)
flask-security 4.0.0-1%2Bdeb11u1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 2,340 kB
  • sloc: python: 12,730; makefile: 131
file content (162 lines) | stat: -rw-r--r-- 5,560 bytes parent folder | download
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