我有flask-login
和一个SQLAlchemy
数据库来验证用户登录尝试。但是将用户名和密码输入与基于令牌的 totp 合并一直是我问题的重点。我正在使用passlib
来管理totp身份验证,该身份验证在数据库用户模型中存储了一个令牌生成器。
我首先想到将所有输入字段放在同一页面上。这意味着用户在访问登录页面时需要填写三个字段:用户名、密码、totp 令牌。
但我希望 totp 令牌显示在单独的页面上。所以,我 随后将登录信息剪切为两页。一个用于用户名和密码,另一个用于 totp 令牌。
我想通过第一页获取用户的登录用户名和密码,然后将其与数据库匹配,以查看它们是否正确以及它们在哪里,它们将被路由到带有 totp 令牌输入的另一个页面。此页面将采用令牌输入,并使用上一页中输入的用户生成的数据库令牌对其进行验证。
正是在为2fa创建如此单独的路线时,我的所有问题都出现了。例如
您必须确保没有人可以访问2fa页面,如果他们尚未通过登录页面并成功输入正确的凭据。
您还必须将有关登录页面中身份验证人员的确切信息保留到 2fa 页面中,以便可以使用数据库中的正确用户验证令牌输入。
而且我不知道如何坚持知道谁经历了第一个登录步骤到第二个登录步骤。我首先想到使用会话存储来存储用户在成功完成第一个登录过程时的 ID,以便第二条路由可以拾取该会话并确定谁在进行身份验证。但随着饼干的到来。这不是那么安全。
那么,如何在 Flask 中安全地实现基于多页令牌的双因素身份验证呢?
我认为如果您可以展示您正在使用的一些代码(相关的模型、路由和模板(,以便我们更好地帮助您,这可能会有所帮助。话虽如此,我认为您正在寻找的关键元素是Flask的对象session
。有了这个,您可以在路由之间保留信息,在本例中为username
.
就像您提到的,您不是在一个模板和路由中验证密码和 2fa 令牌,而是将它们一分为二:
- 用于验证密码的一条路由和模板
- 用于验证 2fa 令牌的路由和模板
例如,查看下面的两条路由和关联注释:
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)
作为"附件",我附上一个"用户"模型的示例,如果这可能对您有所帮助。
PS:我使用了2fa链接的pyotp
模块
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)