1

I have flask-login and an SQLAlchemy db to authenticate user login attempts. But merging username and password inputs with a token-based totp has been the point of my problem. I'm using passlib to manage the totp authentication which has a token generator stored in the database user model.

I first thought of having all the input fields on the same page. Meaning the user would have three fields to complete: username, password, totp token when they visite the login page.

But I wanted the totp token to be displayed on a separate page. So, I subsequently cut the login to two pages. One for the username and password and another for the totp token.

I thought of taking the user's login username and password through the first page, then matching it with the db to see if they were correct and if they where, they would be routed to another page with a totp token input. This page would take the token input and verify it with the database token generated from the user that was entered in the previous page.

It is in creating such a separate route for the 2fa that all my problems arose. For example,

You have to make sure no one could access the 2fa page if they haven't already gone through the login page and successfully entered the right credentials.

You also have to persist the info about who exactly authenticated from the login page into the 2fa page so the token input can be verified with the right user from the db.

And I have no idea how to persist knowledge of who went through the first login step to the second. I first thought of using session storage to store the id of the user when they successfully go through the first login process so the second route could pick up that session and identify who is authenticating. But as cookies go. This isn't that secure.

So how can one securely implement multipage token-based two-factor authentication in Flask?

1 Answers1

2

I think it might help if you can show some of your code you are working with (the associated models, routes and templates) so we can help you better. That being said, I think the key element you are looking for is session object of Flask. With this you can persist information between the routes, in this case the username.

Like you mention, instead of verifying the password and 2fa token in one single template and route, you split these in two:

  • One route and template for verifying the password
  • One route and template for verifying the 2fa token

For example, look at the two routes and associating comments below:

routes.py

from flask import session
@app('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    status = 200
    if request.method == 'POST' and form.validate():
        user = User.query.filter_by(username=form.username.data).first()
        # set the username in the current session
        session['username'] = user.username
        # check the 2FA is implemented
        if user.otp_secret is None:
            return redirect(url_for('app.two_factor_setup'))
        if user and user.validate(form.password.data): # first only validate the password
            #login_user(user) --> move this to the other route ('two_factor_input' in this example)
            return redirect(url_for('app.two_factor_input'))
        else:
            flash('Wrong credentials')
            status = 401
    return render_template('login.html', form=form), status

@app.route('/two_factor_input', methods=['GET', 'POST'])
def two_factor_input():
    form  = Token2FAForm(request.form)
    status = 200
    if 'username' not in session:
        return redirect(url_for('app.login'))
    user = User.query.filter_by(username=session['username']).first()
    if user is None:
        flash('Something went wrong')
        return redirect(url_for('app.login'))
    if request.method == 'POST':
        if user and user.verify_totp(form.token.data):
            login_user(user)
            flash('Succesvol logged in')
            return redirect(url_for('app.index'))
        else:
            flash('Something went wrong')
            return redirect(url_for('app.two_factor_input'))

    return render_template('two_factor_input.html', form=form)

As "attachment" I enclose an example of the 'User' model if this might help you.

PS: I used the pyotp module for the 2fa link

models.py

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

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

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

    @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)


    # methods for 2factor-authentication
    def get_totp_uri(self):
        return 'otpauth://totp/Your App%20Token:{0}?secret={1}&issuer=yourapp'.format(self.username, self.otp_secret)


    def verify_totp(self, token):
        totp = pyotp.TOTP(self.otp_secret)
        return totp.verify(token)
AT90
  • 51
  • 7
  • I'm working on a new project and will be implementing TOTP. I was doing a search to read up on best practices on TOTP w/Flask. Very impressed with your detailed answer! Great job! – wildernessfamily Sep 29 '22 at 17:02
  • 1
    Thanks @wildernessfamily, you're welcome! – AT90 Jun 27 '23 at 12:34