如何在Python和Flask中实现TOTP 2FA

912 阅读7分钟

在Python和Flask中实现TOTP 2FA

双因素认证(2FA)是一种安全协议,它通过要求用户使用两种认证方法来验证他们的身份来保护用户。

近来,大多数组织使用2FA技术来确保其用户的详细信息,并避免黑客获得未经授权的访问的可能性。

双因素认证是使用以下任何一个因素设置的。

  • 占有因素。这个因素使用只有用户才有的东西对用户进行认证,如身份证、用于接收OTP的移动小工具或安全令牌。
  • 生物识别因素。这通常需要用户在认证过程中亲自到场。它可以是指纹或面部识别的形式。
  • 位置和时间因素。这通常使用GPS或VPN检查用户的当前位置。

前提条件

要遵循并完全理解本教程,你需要具备以下条件。

  • Python 3.6或更新的版本。
  • 一个文本编辑器。
  • 对Flask有基本了解。

双因素认证如何工作

从本质上讲,双因素认证的过程包括以下程序。

  1. 用户使用电子邮件和密码(知识因素)进行自我认证。
  2. 平台确认用户的信息,并要求提供第二个认证技术。
  3. 平台生成一个一次性密码(OTP),并将其发送到只有用户可以访问的设备上(占有因素)。
  4. 用户将收到的OTP提供给平台,平台会验证信息并授权给用户。

双因素认证的重要性

  • 它为用户提供了有保证的账户安全。
  • 它可以保护平台免受数据泄露的影响。
  • 它提高了客户对一个组织的信心。
  • 它确保只有预定的用户可以评估受保护的信息。

基于时间的一次性密码(TOTP)

基于时间的一次性密码(TOTP)是在应用程序中实施双因素认证的一种常见方式。它的工作原理是要求用户提供一个令牌,通常以短信、电子邮件或生成的密文方式发送到用户的设备上,并有一个过期时间。它将提供的令牌与实际生成的令牌进行比较,如果令牌匹配,则对其进行认证。

谷歌认证器

谷歌认证器使用谷歌制作的基于软件的认证技术,使用TOTP和基于HMAC的一次性密码(HOTP)实现2FA,用于认证应用程序的用户。它是开放认证(OATH)的一部分。

基于哈希的消息认证码(HMAC)是一种使用哈希函数和密匙来计算消息认证码的技术。

TOTP认证器应用如何工作

从本质上讲,用认证器进行认证的过程涉及以下程序。

  1. 网站要求用户提供一个由认证器应用程序生成的一次性密码。
  2. 然后,网站使用认证器应用程序和它自己都知道的一个种子值生成另一个令牌。
  3. 如果新生成的令牌与用户提供的令牌相匹配,网站就会继续对用户进行认证。

用Python和Flask实现TOTP 2FA

安装所需的库

双因素认证通常用于网络应用,作为用户访问服务器时的一个额外的安全层。使用Python,让我们建立一个Flask应用程序,并使用Google Authenticator的双因素认证来保护它。

首先,你必须安装Flask网络框架、Flask-BootstrapPyOTP库,你将用它们来构建服务器和实现双因素认证。

在终端中,输入。

pip install flask
pip install pyotp
pip install flask-bootstrap4

构建一个简单的Flask服务器

你将编写设置Flask服务器的代码。

首先创建一个名为app.py 的文件,并在其中保存以下代码。

# importing needed libraries
from flask import *
from flask_bootstrap import Bootstrap

# configuring flask application
app = Flask(__name__)
app.config["SECRET_KEY"] = "APP_SECRET_KEY"
Bootstrap(app)


# homepage route
@app.route("/")
def index():
    return "<h1>Hello World!</h1>"


# running flask server
if __name__ == "__main__":
    app.run(debug=True)

在上面的代码中,你创建了一个Flask服务器,当索引页打开时渲染文本"Hello World!" 。运行该服务器后,你会得到一个类似于下图的响应。

Hello World

Flask中的单因素认证

接下来我们将编写代码,使用用户名和密码来验证用户。为了简单起见,我们将硬编码凭证,应用程序将匹配。

更新app.py 文件,添加下面的代码。

# login page route
@app.route("/login/")
def login():
    return render_template("login.html")

你还将创建一个名为login.html 的文件,该文件将存储在templates 目录中,并在其中保存以下代码。

{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Flask + 2FA Demo</h2>
      </div>
    </div>
    <div class="col-lg-6">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="username">Username</label>
          <input type="text" class="form-control" id="username" name="username" required>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" class="form-control" id="password" name="password" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-primary">Authenticate User</button>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}

Login

你还将编写一个路由来处理POST 向登录页面发出的请求并对其进行认证。

更新app.py 文件,加入下面的代码。

# login form route
@app.route("/login/", methods=["POST"])
def login_form():
    # demo creds
    creds = {"username": "test", "password": "password"}

    # getting form data
    username = request.form.get("username")
    password = request.form.get("password")

    # authenticating submitted creds with demo creds
    if username == creds["username"] and password == creds["password"]:
        # inform users if creds are valid
        flash("The credentials provided are valid", "success")
        return redirect(url_for("login"))
    else:
        # inform users if creds are invalid
        flash("You have supplied invalid login credentials!", "danger")
        return redirect(url_for("login"))

当在表单中使用无效的凭证时,你应该得到一个类似于下面的图像。

invalid login

而当使用有效凭证时,则会出现与下面类似的图像。

valid login

请注意,为应用程序创建的有效凭证是username: testpassword: password

Python中的TOTP 2FA认证

要使用PyOTP生成TOTP,你需要实例化PyOTP库的TOTP 类并调用now 方法。

下面是一个演示该功能的Python代码样本。

import pyotp

# generating TOTP codes with provided secret
totp = pyotp.TOTP("base32secret3232")
print(totp.now())

你可以继续使用verify 方法来验证生成的令牌。

你可以用下面的代码来做这件事。

import pyotp

# verifying TOTP codes with PyOTP
totp = pyotp.TOTP("base32secret3232")
print(totp.verify("492039"))

你可以使用下面的代码来生成和验证基于计数器的OTPs。

import pyotp

# generating HOTP codes with PyOTP
hotp = pyotp.HOTP("base32secret3232")
print(hotp.at(0))
print(hotp.at(1))
print(hotp.at(1401))

# verifying HOTP codes with PyOTP
print(hotp.verify("316439", 1401))
print(hotp.verify("316439", 1402))

PyOTP还提供了一个辅助库来生成秘密密钥,以启动TOTPHOTP 类。

你可以用下面的代码来做这件事。

import pyotp

# generating random PyOTP secret keys
print(pyotp.random_base32())

你可能希望秘钥的格式是一个十六进制编码的字符串。

import pyotp

# generating random PyOTP in hex format
print(pyotp.random_hex()) # returns a 32-character hex-encoded secret

Flask中的TOTP 2FA认证

你将编写代码,向用户提供设置TOTP 2FA的页面。首先更新app.py 文件中的login 路由,以便在认证成功后将用户重定向到2FA页面。

# redirecting users to 2FA page when creds are valid
if username == creds["username"] and password == creds["password"]:
    return redirect(url_for("login_2fa"))

你还将创建login_2fa 路由,负责处理TOTP 2FA。

app.py 文件中添加以下代码。

# 2FA page route
@app.route("/login/2fa/")
def login_2fa():
    # generating random secret key for authentication
    secret = pyotp.random_base32()
    return render_template("login_2fa.html", secret=secret)

你还将创建一个名为login_2fa.html 的文件,该文件将存储在templates 目录中,并在其中保存以下代码。

{% extends "bootstrap/base.html" %}

{% block content %}
<div class="container">
  <div class="row justify-content-center">
    <div class="col-lg-12">
      <div class="jumbotron text-center p-4">
        <h2>Flask + 2FA Demo</h2>
        <h4>Setup and Authenticate 2FA</h4>
      </div>
    </div>
    <div class="col-lg-5">
      <form>
        <div>
          <h5>Instructions!</h5>
          <ul>
            <li>Download <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en&gl=US" target="_blank">Google Authenticator</a> on your mobile.</li>
            <li>Create a new account with <strong>setup key</strong> method.</li>
            <li>Provide the required details (name, secret key).</li>
            <li>Select time-based authentication.</li>
            <li>Submit the generated key in the form.</li>
          </ul>
        </div>
        <div class="form-group">
          <label for="secret">Secret Token</label>
          <input type="text" class="form-control" id="secret" value="{{ secret }}" readonly>
        </div>
        <div class="text-center">
          <button type="button" class="btn btn-primary" onclick="copySecret()">Copy Secret</button>
        </div>
      </form>
    </div>
    <div class="col-lg-7">
      {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
      {% for category, message in messages %}
      <div class="alert alert-{{ category }}" role="alert">
        {{ message }}
      </div>
      {% endfor %}
      {% endif %}
      {% endwith %}
      <form method="POST">
        <div class="form-group">
          <label for="otp">Generated OTP</label>
          <input type="hidden" name="secret" value="{{ secret }}" required>
          <input type="number" class="form-control" id="otp" name="otp" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-primary">Authenticate User</button>
        </div>
      </form>
    </div>
  </div>
</div>

<script>
  function copySecret() {
    /* Get the text field */
    var copyText = document.getElementById("secret");

    /* Select the text field */
    copyText.select();
    copyText.setSelectionRange(0, 99999); /*For mobile devices*/

    /* Copy the text inside the text field */
    document.execCommand("copy");

    alert("Successfully copied TOTP secret token!");
  }
</script>
{% endblock %}

Generating OTP

你还将编写一个路由,以处理向2FA页面提出的POST ,并对其进行认证。

更新app.py 文件,加入下面的代码。

# 2FA form route
@app.route("/login/2fa/", methods=["POST"])
def login_2fa_form():
    # getting secret key used by user
    secret = request.form.get("secret")
    # getting OTP provided by user
    otp = int(request.form.get("otp"))

    # verifying submitted OTP with PyOTP
    if pyotp.TOTP(secret).verify(otp):
        # inform users if OTP is valid
        flash("The TOTP 2FA token is valid", "success")
        return redirect(url_for("login_2fa"))
    else:
        # inform users if OTP is invalid
        flash("You have supplied an invalid 2FA token!", "danger")
        return redirect(url_for("login_2fa"))

当提供一个无效的令牌时,你应该得到一个类似于下面的图像。

invalid OTP

当提供一个有效的令牌时,你应该得到一个类似于下面的图像。

valid OTP

结论

在这篇文章中,我们学习了双因素认证的概念,讨论了不同的2FA因素,包括占有因素、生物识别因素和其他因素。

我们还强调了在应用程序中实施2FA的重要性,并使用Google Authenticator和PyOTP将双因素认证整合到一个Flask应用程序中。