2015-02-17T01:26:04Z

Two Factor Authentication with Flask

In this article I'm going to introduce an authentication scheme known as two factor authentication. As the name implies, this method requires the user to provide two forms of identification: a regular password and a one-time token. This greatly increases account security, because a compromised password alone is not enough to gain access, an attacker also needs to have the token, which is different every time. You can see me do a short demonstration of this technique in the video above.

As usual, this article includes a complete example that implements this authentication technique in a Flask application. You may think this is going to be an advanced article that needs complex cryptographic techniques, specialized hardware and/or proprietary libraries, but in reality it requires none of the above. The solution is relatively simple to add if you already have username and password authentication in place, and can be done entirely with open standards and open-source software. There are even open-source token generation apps for your Android or iOS smartphone!

The Example Application

As mentioned above, in this article I present a complete example application. This application demonstrates how to do two factor authentication in a web application that uses Flask and Flask-Login. If you would like to try a live version of this application, click the link below and follow the instructions to register an account. Note that you will be asked to install a free app on your smartphone as part of the registration process (Android and iOS are both supported).

Try the example application now!

The source code for the example application is hosted in the following Github repository: https://github.com/miguelgrinberg/two-factor-auth-flask. See the README file for installation instructions.

Introduction to Two Factor Authentication

Letting users authenticate to an application just with a username and password combination is inherently risky, because when the password is compromised the attacker obtains full access. To reduce that risk, security conscious applications can implement multi-factor authentication, which requires the user to provide additional additional proofs of identity. With two factor authentication, the user must provide the password, plus a second authentication factor.

Some of these accessory identity verification schemes are based on a physical characteristic of the user, such as a fingerprint, or an iris scan. Another common way to prove identity is with a physical object that the user carries at all times, such as a card with a magnetic strip, or a portable token generator.

If you work for a company that lets you connect to the office from home through a VPN, chances are you are already familiar with two factor authentication, and you already have a token generation device or app similar to these:

For the example application in this article I'm going to concentrate on this type of authentication factor, which is based on one-time password generation algorithms.

One-Time Passwords

The idea behind one-time passwords is that they are only valid for a single login session. These passwords are generated algorithmically by a hardware device or a smartphone app. To validate a one-time password, the server runs the same algorithm and compares the result with the password provided by the user. The difference with a traditional password is that the user does not need to memorize anything, the generated password is displayed by the token generation device and the user just copies it to the login form.

There are many one-time password algorithms, most are proprietary, but there are a few open standards, of which HOTP and TOTP are the most commonly used.

The HOTP algorithm, short for HMAC-based One-time Password, is described in RFC 4226. This algorithm generates tokens based on a secret and a counter, both known by the token generation device and the authentication server. Each time a token is used the counter is incremented on both sides, and that makes the algorithm generate a different token for the next login attempt.

The TOTP algorithm, short for Time-based One-time Password, is described in RFC 6238. This standard also uses a shared secret, but deals away with the counter, which is replaced by the current time. With this algorithm the token changes at a predefined time interval, usually every 30 seconds.

The benefit of TOTP over HOTP is that tokens are a function of time, and thus are constantly changing. That means that even if an attacker can take a peek at the current token displayed on your smartphone app, a few seconds later it will be superseded by a new one. The disadvantage of TOTP is that it requires the token generator and the authentication server to have their clocks set to approximately the same time. This is not a problem for the smartphone, but on the server it is recommended that you run an NTP client to keep the clock from drifting.

For the example application that I present in this article I'm going to use TOTP, but it should be fairly easy to adapt the application to use HOTP.

The First Factor: Password Authentication

I'm not going to spend a lot of time describing how to do password authentication because I have written extensively about it. But I feel it is always good to repeat a few best-practices to keep user passwords safe:

  • Passwords must never be stored in the database. Store a password hash instead.
  • To verify a password, calculate its hash, then compare it against the hash stored in the database.
  • Always use secure HTTP to transmit forms that contain passwords.

If you would like to see a fairly complete implementation of password authentication that includes user registration, email verification and password resets, you will find one in my book. The source code featured in the book is in this Github repository.

For this example I used a much simpler setup, because I did not want to complicate the example with features that are largely unrelated to this article. I have basically skipped email verification and password reset options, but note that these are necessary for a production application, even when two-factor authentication is added to the mix.

For this application, I used Flask-SQLAlchemy, Flask-Login, Flask-WTF and Flask-Bootstrap, so for those that have read my other Flask tutorials this is going to be a fairly familiar application. The User model class is shown below:

from werkzeug.security import generate_password_hash, check_password_hash
from flask.ext.login import UserMixin
from app import db, lm

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True)
    password_hash = db.Column(db.String(128))

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

As you can see, I'm hashing the passwords using Werkzeug's hashing functions. Note that the password property allows me to say user.password = 'some-password', and this will automatically trigger the hash of the password to be stored in user.password_hash. The original password is then discarded.

The user registers an account by providing a username and a password. The password is asked twice to ensure it is typed correctly. Here is the Flask-WTF form that handles this:

class RegisterForm(Form):
    username = StringField('Username', validators=[Required(), Length(1, 64)])
    password = PasswordField('Password', validators=[Required()])
    password_again = PasswordField('Password again',
                                   validators=[Required(), EqualTo('password')])
    submit = SubmitField('Register')

Once users are registered, they can login by entering the username and the password on the login form, which you can see below:

class LoginForm(Form):
    username = StringField('Username', validators=[Required(), Length(1, 64)])
    password = PasswordField('Password', validators=[Required()])
    submit = SubmitField('Login')

The route that handles the registration form is pretty straightforward, so I'm not going to show it here. It basically takes the username and the password submitted by the user and adds then as a new user to the database. Below you can see the route that logs a user in:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated():
        # if user is logged in we get out of here
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.verify_password(form.password.data):
            flash('Invalid username or password.')
            return redirect(url_for('login'))
        # log user in
        login_user(user)
        flash('You are now logged in!')
        return redirect(url_for('index'))
    return render_template('login.html', form=form)

Here you can see how the password verification is done, simply by calling user.verify_password() in the model.

If you look at the commit history in the Github repository, you can find a commit that adds all the code related to password authentication together. You can also checkout that commit and try the application at that stage if you like. Once you feel you have a good understanding of the application at this stage, you are ready to move on to the tokens.

The Second Factor: TOTP Tokens

A search on pypi revealed a few packages that implement the TOTP algorithm. I tested a few of them and decided on onetimepass, a small library that supports HOTP and TOTP and is compatible with Python 2 and 3. If you want to learn how these tokens are calculated, I recommend that you read the source code, which is short and easy to understand.

The User Model

Now that the problem of calculating and verifying tokens is solved, let's look at the changes that add token support to the user model:

import os
import base64
import onetimepass

class User(UserMixin, db.Model):
    # ...
    otp_secret = db.Column(db.String(16))

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        if self.otp_secret is None:
            # generate a random secret
            self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')

    # ...

    def get_totp_uri(self):
        return 'otpauth://totp/2FA-Demo:{0}?secret={1}&issuer=2FA-Demo' \
            .format(self.username, self.otp_secret)

    def verify_totp(self, token):
        return onetimepass.valid_totp(token, self.otp_secret)

The user model gets an additional field called otp_secret that stores the shared secret that the TOTP algorithm uses as input. This should be a binary string of length 10 encoded as a base32 string, which makes it a printable string with 16 characters. The __init__() constructor sets it to a random string if a value for this field isn't given as an argument.

The get_totp_uri() function returns an authentication URI. This is used to transfer the shared secret and additional account information to the smartphone. This URI will be rendered as a QR code that you have to scan with your phone. Below you can see the structure of these URIs:

otpauth://<protocol>/<service-name>:<user-account>?secret=<shared-secret>&issuer=<service-name>

Here the <protocol> can be totp or hotp. The <service-name> is the name of the service or application that the user is authenticating to. The <user-account> can be the username, the user's email address or anything that identifies the user account. The <shared-secret> is the code that is used to seed the token generator algorithm. The issuer argument is normally set to the service name. A period optional argument can also be used to change the interval for token changes, which defaults to 30 seconds. For more information about these URIs, see the documentation on the Google Authenticator wiki.

Finally, the verify_totp() function takes a token as input, and validates using the support provided by the onetimepass package.

User Registration

There are several possible options to consider when implementing the user registration flow. For some applications it may make sense to leave user registration as is, and then give users the option to optionally enable two factor authentication if they wish so. For other applications, two factor may be mandatory, so it is incorporated into the registration process.

For this application, I have opted for the latter, so immediately after submitting the user registration page the user is presented with a two factor authentication setup page that looks like this:

Here the user needs to start the token generator app on the smartphone, and use it to scan the QR code. This is all it takes to register the shared secret and account information on the phone. After this step is done, the user can go to the login page and login using password and token for the first time.

The changes to implement the QR code page are not as scary as they may seem. First, the original registration route is changed to redirect to a new route that I called two_factor_setup instead of sending the user to the login page. Before redirecting, it adds the username to the user session, so that the QR code page knows what user is registering. Here are the changes to the registration route:

@app.route('/register', methods=['GET', 'POST'])
def register():
    # ...
    form = RegisterForm()
    if form.validate_on_submit():
        # ...

        # redirect to the two-factor auth page, passing username in session
        session['username'] = user.username
        return redirect(url_for('two_factor_setup'))
    return render_template('register.html', form=form)

And below you can see the implementation of the two_factor_setup route:

@app.route('/twofactor')
def two_factor_setup():
    if 'username' not in session:
        return redirect(url_for('index'))
    user = User.query.filter_by(username=session['username']).first()
    if user is None:
        return redirect(url_for('index'))
    # since this page contains the sensitive qrcode, make sure the browser
    # does not cache it
    return render_template('two-factor-setup.html'), 200, {
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        'Pragma': 'no-cache',
        'Expires': '0'}

After validating that there is a username stored in the user session, this route ensures the user exists, and if it does it just renders a new template called two-factor-setup.html. This page is served with extra headers that tell the browser to not do any caching. The reason is that this page will include a QR code that can give an attacker access to the time based tokens, so it's best to take precautions and make sure there are no copies of the QR code lost in a cache.

The two-factor-setup.html template is also fairly simple:

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block page_content %}
    <h1>Two Factor Authentication Setup</h1>
    <p>You are almost done! Please start FreeOTP on your smartphone and scan the following QR Code with it:</p>
    <p><img id="qrcode" src="{{ url_for('qrcode') }}"></p>
    <p>I'm done, take me to the <a href="{{ url_for('login') }}">Login</a> page!</p>
{% endblock %}

The page includes a reference to the QR code image, but the URL for this image is not a usual image link, it is a dynamic URL generated with Flask's url_for() function. This is because the QR code image has to be generated specifically for each user, so a Flask route is invoked to do this work.

This qrcode route is different than the rest, because instead of returning HTML it returns image data, in this case in SVG format. To generate the QR code I'm using package pyqrcode. Here is the code for this route:

@app.route('/qrcode')
def qrcode():
    if 'username' not in session:
        abort(404)
    user = User.query.filter_by(username=session['username']).first()
    if user is None:
        abort(404)

    # for added security, remove username from session
    del session['username']

    # render qrcode for FreeTOTP
    url = pyqrcode.create(user.get_totp_uri())
    stream = StringIO()
    url.svg(stream, scale=5)
    return stream.getvalue().encode('utf-8'), 200, {
        'Content-Type': 'image/svg+xml',
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        'Pragma': 'no-cache',
        'Expires': '0'}

Here once again I check that the username is in the session and that it is a known user. If any of those checks fail, I return a 404 error code, which to the browser will look like it requested an image file that does not exist. If the user is valid, I quickly remove it from the session, because once the user requests the QR code I want to make sure it this image cannot be requested again. This means that if the user does not scan the QR code in this only occasion it is presented, then the account is not going to be accessible.

The information stored in the QR code is the URL that includes the TOTP data. This is what most TOTP smartphone apps expect, something that the Google Authenticator app started and everyone else copied. Recall that I've added a method that generates this URL as User.get_totp_uri() above. The QR code rendering simply involves using the pyqrcode functions to render the TOTP URL as a SVG image, which I save to a StringIO stream in memory. This buffer is then returned as a response with the correct content type for SVG images. I also threw in the headers that disable caching for the image, as I did with the parent HTML page.

The user now can scan the QR code with a TOTP enabled smartphone app, and as soon as that is done the registration process is complete. Pretty cool, right?

Login

The only piece that is left to do is to extend the login form to accept a token, and also to validate it. I mentioned above that applications may opt to make two factor authentication optional or mandatory. If this is made optional, then the login form does not change, users enter their username and password, and upon verification the application can find out if two factor authentication is enabled and present an additional form where the user enters the token. In the case of this application, however, two factor setup is required for all accounts, so I decided to make a single login dialog that accepts username, password and token together.

Here is the improved login form, which just gets an extra field for the token.

class LoginForm(Form):
    username = StringField('Username', validators=[Required(), Length(1, 64)])
    password = PasswordField('Password', validators=[Required()])
    token = StringField('Token', validators=[Required(), Length(6, 6)])
    submit = SubmitField('Login')

And then the login route is simply enhanced with an additional validation check:

@app.route('/login', methods=['GET', 'POST'])
def login():
    # ...
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.verify_password(form.password.data) or \
                not user.verify_totp(form.token.data):
            flash('Invalid username, password or token.')
            return redirect(url_for('login'))

        # log user in
        # ...
    return render_template('login.html', form=form)

Here the only addition is the call to user.verify_totp(), the method that I added to the user model to check tokens using the onetimepass package. Note that when authentication fails I do not give any clues about what part has failed, the error message is pretty generic.

And that is all, the application with two factor authentication is now complete!

Possible Improvements

This application is focused on simplicity, I tried to not over complicate it so that the core concepts of working with tokens are easy to understand. However, in a real world application you may want to make things a bit more robust, so I'm going to discuss some of these additions that may be useful.

As I mentioned above, for many types of applications using two factor authentication should be optional, so it is only for users that want to take advantage of the stronger account security. For these applications, user registration and login pages do not need to change. Instead, a settings page allows users to enable and set up two factor authentication once they are logged in. The login process is then split in two parts, the first part is the regular username and password login, and then for those users that enabled two factor authentication a second page requests the token.

A common practice for applications that allow users to edit sensitive account settings is to request the user to authenticate again right before making account changes. For example, to change your password, applications typically ask you to enter your old password first. When two factor authentication is used, the application should ask the user to provide the current token as well, but it is important that this is a new token that was not used before. Imagine that somehow an attacker was able to crack your password, and also spied on your phone and knows the current code. That means that for up to 30 seconds you are vulnerable. If the attacker logs in to your account and then quickly goes to the account settings page entering the password and the same token again, your account is compromised. Now the attacker can disable two factor authentication and gain full access in the future. To avoid this, an application should save any tokens that were consumed to the user database and not allow the same token to be used a second time. The attacker will be forced to wait those 30 seconds before entering the settings page, so it gets even harder to have the account compromised.

An alternative to use a smartphone app to generate the tokens is to have the server send an email or SMS with the current code. The onetimepass package that I used for the example application not only provides a function to validate TOTP codes, it has a function that just returns the current code, so you can then send it to the user as you see appropriate. For this type of workflow it may be necessary to increase the period at which tokens change.

Account recovery is harder when two factor authentication is used. If an application allows users to regain access to their accounts without having a valid token, then an attacker can take advantage of this facility as well. Typically users that are locked out of their accounts have to contact an administrator and have their accounts reset manually. You can also opt to add another form of verification, such as security questions, but of course this in part undermines the increased account security.

As mentioned in the reddit discussion of this article, there are a couple of implementation details that can be improved to make the application more secure. Storing the OTP secret and the hashed passwords in the same table can be seen as a security risk, because in the event of a security breach that gives the attacker access to the database, both will be accessible. To mitigate this risk, you could choose to store these two sensitive items in different database tables, or even better, different databases altogether. Encrypting the OTP secret, maybe using Flask's SECRET_KEY as encryption key, can also help. In all cases secure HTTP must be used for all communications that include passwords and the OTP secret (which is encoded in the QR code).

Conclusion

I hope this was a useful article. I would love to hear what you build with the techniques I presented in this article, so please let me know of any projects you make. As always, feel free to write your questions below.

Miguel

23 comments

  • #1 Kai said 2015-02-19T20:28:50Z

    That was very interesting and I must say you have a very good way of explaining things in aneasy and understandable way. Since I'm still building my REST-API based on your tutorial and build a Falsk-site... this willbe the next to include!!!

  • #2 Laowai said 2015-02-26T19:42:00Z

    Always excited to read your new articles. I'm actually reading through your flask book for the second time right now =)

  • #3 Dee Odee said 2015-03-19T12:59:27Z

    great tutorial. just wanted to say that the code on this page (haven't looked at the repo code yet) is missing a `.format(` (in `User.get_totp_uri()`)

  • #4 Miguel Grinberg said 2015-03-21T21:48:27Z

    @Dee: thanks, I corrected the mistake here. The code on Github was correct.

  • #5 nwaomachux said 2015-04-01T14:46:13Z

    I used the one hosted on Heroku, thanks for help.

  • #6 Bob Jansen said 2015-05-26T16:56:19Z

    I appreciate your clean, well-documented approach, Miguel. I will read much more of your work. My interest is in interfacing my household electronics (past and real-time temperatures, energy usage, lights controlled, etc.) through the Raspberry Pi as dynamic web forms with Python and Flask. I have lots of VB programming experience and a good bit with python, not to mention various microcontrollers and their languages, but the raspberry will be a new challenge. Just getting 2 separate network interfaces working through the ethernet and wlan devices has been problematic. Hopefully I will be able to share some success details with you along the way.

  • #7 Jan said 2015-06-14T18:13:11Z

    Thank you for the tutorial Miguel! Much appreciated. For those that want to use this in Python 3+, use BytesIO instead of StringIO and remove the encoding part like this: return stream.getvalue(), 200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0'} That should make it work.

  • #8 Bang said 2015-06-26T06:22:20Z

    I register and use Google Authenticator to scan qrcode successfully. But when login with token that provided by Google Authenticator the page at http://two-factor-auth.herokuapp.com/login said Invalid username, password or token.

  • #9 Bang said 2015-06-26T08:17:40Z

    If someone failed to login, just install ClockSync(android) to update correct time for your phone. So the token generated by Google Authenticator or other alike apps will be correct. I solved it that way.

  • #10 Jay Tee said 2015-07-12T11:51:28Z

    Can you tell me what the wtf.html looks like? Trying your approach with the two-factor but the only thing stopping me is the wtf.html file in the bootstrap directory. Thanks in advance!

  • #11 Miguel Grinberg said 2015-07-16T06:01:46Z

    @Jay: wtf.html is a file that contains some macros for form generation. If you get the project from the github repository and follow the instructions to run you'll get all the dependencies.

  • #12 Jeremy Epstein said 2015-08-14T01:13:54Z

    Great article, Miguel - I didn't realise that it's so easy to implement basic 2FA these days. For my projects (small company, clients with limited budget), I had ruled it out as requiring too much integration code / expensive 2FA cloud providers. But I see that it's a trivial amount of code in Flask (which is my framework of choice these days), and that there are no provider costs with an app like FreeOTP. Of course, sending codes via SMS needs to be done via some sort of premium service, but I guess that SMS isn't necessary for all use cases (although personally, I prefer it over having to install an app on my phone, just to log in to a web site).

  • #13 Miguel Grinberg said 2015-08-14T05:03:56Z

    @Jeremy: twilio is fairly accessible price wise for sending SMS: https://www.twilio.com/sms/pricing.

  • #14 David said 2015-09-12T16:31:31Z

    Any suggestions on unittesting with 2FA?

  • #15 Miguel Grinberg said 2015-09-13T01:43:44Z

    @David: Well, if we trust that the 2FA and QR-code libraries work and agree that they do not need to be tested, what's left to test is how your own application uses the libraries. This involves the following: #1: Verify that the URI that is encoded as a QR code is correct. For this you can mock the secret generator (os.random in my example) so that the URI that you get is predictable. #2: Verify that the application responds to the 2FA token verification in the correct way. For this, you can mock the 2FA verify function (onetimepass.valid_totp in my example). You can then write two tests with the mocked function returning True and False. For a simple unit test, you can just invoke the User.verify_totp method and ensure it responds correctly to the mocked function. For a more advanced test you can use Selenium and do an end-to-end test. Hope this helps!

  • #16 Brian Munroe said 2015-10-16T22:10:43Z

    Thanks for the great article, Miguel! For anyone having trouble getting the example application to work (I tried both Google Authenticator and FreeOTP, on iOS 9), it is probably related to clock skew. Bang had mentioned a fix for Android, but there doesn't appear to be any easy way to correct it on the iPhone. Instead, I widened the window in the onetimepass.valid_totp() method: def verify_totp(self, token): return onetimepass.valid_totp(token, self.otp_secret,window=2) The default is 1, but setting it to two seemed to do the trick.

  • #17 Miguel Grinberg said 2015-10-17T00:10:10Z

    @Brian: note that the default window is 0, which tells onetimepass to only check the code for the current time slot. If you set it to 1, then in addition to the current time slot, the next and previous time slots will be checked. By making it 2 there will be 5 time slots checked, the current one plus two on each side. Since each time slot is 30 seconds, that means that a code will be valid for 2.5 minutes, instead of the intended 30 seconds. While this isn't terrible, I would figure out where is the source of the time difference between the smartphone and the server. Maybe all you need is to add ntp to your server.

  • #18 Brian Munroe said 2015-10-19T21:02:14Z

    @Miguel: Wow, I have no idea where i got that default from, you are most correct! I was able to solve my window problem (now back at window=0) by logging into the corporate domain and letting ntpd do its thing. It had been a while and my clock seems to have drifted enough to cause problems. Thanks again for the great article, I learned a bunch!

  • #19 K. Summerland said 2015-12-19T10:40:04Z

    Great job. I think there is a typo where 'then' should read 'them'? Found in this "the password submitted by the user and adds then as a new user to the database." A question regarding the use render_template. How did you know about that use? Where is that documented? Any other uses we can refer to? I ask because the method is defined in the Flask docs as follows, no mention of your use case there. """ From FLASK documentation flask.render_template(template_name_or_list, **context) Renders a template from the template folder with the given context. Parameters • template_name_or_list – the name of the template to be ren- dered, or an iterable with template names the first one existing will be rendered • context – the variables that should be available in the context of the template. """ Thanks

  • #20 Miguel Grinberg said 2015-12-22T19:57:32Z

    @K: Which use do you refer to? I'm using the function according to the documentation. For any response, using render_template or not, you can provide up to three values, the response text, the status code and the headers. Most times the status and headers are not customized, so Flask provides default ones.

  • #21 Bas said 2016-03-26T08:45:24Z

    Hi, Whenever I go to the two-facrtor page, the QR image doesn't show and I get the following error: unicode argument expected, got 'str' Anyone has an idea what might be wrong? Thanks, Bas

  • #22 bas said 2016-03-26T09:11:17Z

    Solved: url = pyqrcode.create(user.get_totp_uri()) stream = BytesIO() url.svg(stream, scale=3) return stream.getvalue(), 200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0'} Answer on Python version 3, works for Pyton 2.7 too

  • #23 Bas said 2016-03-29T18:18:35Z

    To avoid when updating the session(with session.merge), like updating the user name, it is better to change the model from: if self.otp_secret is None: # generate a random secret self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') to: if self.id is None: # generate a random secret self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') As with a session.merge statement the otp_secret gets renewed. If one needs to generate a new OTP secret, simply restart the QRprocess, and merge the required fields.

Leave a Comment