0

我有flask-login一个SQLAlchemy数据库来验证用户登录尝试。但是将用户名和密码输入与基于令牌的 totp 合并一直是我的问题所在。我passlib用于管理 totp 身份验证,该身份验证具有存储在数据库用户模型中的令牌生成器。

我首先想到将所有输入字段放在同一页面上。这意味着用户在访问登录页面时需要填写三个字段:用户名、密码、totp 令牌。

但我希望 totp 令牌显示在单独的页面上。所以,我随后将登录页面剪成了两页。一个用于用户名和密码,另一个用于 totp 令牌。

我想通过第一页获取用户的登录用户名和密码,然后将其与 db 匹配以查看它们是否正确以及它们在哪里,它们将被路由到带有 totp 令牌输入的另一个页面。此页面将获取令牌输入并使用从用户在上一页中输入的数据库令牌生成的验证它。

正是在为 2fa 创建这样一个单独的路线时,我的所有问题都出现了。例如,

如果他们尚未通过登录页面并成功输入正确的凭据,您必须确保没有人可以访问 2fa 页面。

您还必须将有关谁从登录页面经过身份验证的信息持久保存到 2fa 页面中,以便可以使用数据库中的正确用户验证令牌输入。

而且我不知道如何坚持知道谁经历了第一个登录步骤到第二个步骤。我首先想到的是在用户成功完成第一次登录过程时使用会话存储来存储用户的 id,以便第二条路由可以获取该会话并识别谁在进行身份验证。但随着饼干的出现。这不是那么安全。

那么如何在 Flask 中安全地实现基于多页令牌的两因素身份验证呢?

4

1 回答 1

0

我认为如果你能展示你正在使用的一些代码(相关的模型、路由和模板)可能会有所帮助,这样我们就可以更好地帮助你。话虽如此,我认为您正在寻找的关键元素是sessionFlask 的对象。有了这个,您可以在路由之间保留信息,在本例中为username.

就像您提到的那样,不是在一个模板和路由中验证密码和 2fa 令牌,而是将它们一分为二:

  • 验证密码的一种途径和模板
  • 用于验证 2fa 令牌的一种路线和模板

例如,查看下面的两条路线和关联注释:

路线.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)

作为“附件”,我附上了一个“用户”模型的示例,如果这可能对您有所帮助。

PS:我使用pyotp了 2fa链接的模块

模型.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)
于 2020-09-07T16:45:12.747 回答