Find Us

Address
123 Main Street
New York, NY 10001

Hours
Monday—Friday: 9:00AM–5:00PM
Saturday & Sunday: 11:00AM–3:00PM

Categories

How To Implement Biometric 2FA in a Cryptocurrency Wallet with Python, Flask and TypingDNA

Before we get started, we wanted to let you know that TypingDNA has launched a better 2FA solution. TypigDNA Verify 2FA – replace SMS 2FA codes with better UX: Just type 4 words! Take a look here.

This article focuses on implementing biometric two-factor authentication (2FA) and risk-based authentication (RBA) in a cryptocurrency wallet built with Python and Flask using the TypingDNA Authentication API.

TypingDNA helps protect user accounts with robust typing biometrics analysis, accurately and passively. With TypingDNA, you can learn and match your users’ typing patterns and use that information for multi-factor authentication (MFA) on your web platforms.

This article includes a step-by-step guide to integrating TypingDNA Authentication API into a Python web application. We will add an extra layer of security to user authentication and authenticate users before doing risk-based actions like withdrawal. To learn more about TypingDNA, visit the official website.

Table of Contents

  1. Introduction to TypingDNA
  2. Setting up our demo application
  3. Enrolling new users into TypingDNA
  4. Authenticating users with TypingDNA
  5. Adding 2FA fallback for user authentication
  6. Risk-based authentication with TypingDNA
  7. Conclusion

A Brief Introduction to TypingDNA

Of course, we now know TypingDNA is a company that provides a biometrics authentication API. But how does it authenticate users? 

At its core, TypingDNA listens and records users’ keystrokes as they type to analyze their patterns using its biometrics engine. TypingDNA is used in cases where you need to be sure the intended user is the one acting, such as resetting account passwords, multi-factor authentication, risk-based authentication, or as an alternative for OTPs. 

TypingDNA is not constrained to specific use cases or authentication stacks and can be incorporated any place within your architecture where end users are typing. 

We will be exploring most of this together momentarily. But if you’re eager to learn more, check out the TypingDNA documentation

Getting Started with TypingDNA

To get started with TypingDNA, create an account on their platform.

Next, we will copy our API keys (`apiKey` and `apiSecret`) to be stored somewhere secure and easily retrievable. The API keys allow any application we will be building to easily communicate with our TypingDNA account via the REST API.

Setting Up Our Flask Application

In this tutorial, we will be starting with our frontend and backend resources (i.e., Python, Flask, and Bootstrap) for our online cryptocurrency wallet. After that, we will make our way to the two-factor authentication with TypingDNA. 

For the sake of convenience, I have written a Flask application with a Bootstrap user interface that we will use in this article. To get started, you will need to clone my repository and initialize the application like so:

git clone https://github.com/LordGhostX/typingdna-trx-wallet
cd typingdna-trx-wallet

Next, we will be installing all the external libraries our project requires to run. In your terminal, type:

cd without-typingdna
pip install -r requirements.txt

At this point, we want to customize our application settings. Open your `app.py` file and make changes where necessary.

Finally, let’s run our application to make sure it’s working. In your terminal, type:

python app.py
Two-factor Authenticated Crypto Wallets
Crypto Wallets Two Factor Authentication Setup

Enrolling New Users into TypingDNA

Downloading TypingDNA JavaScript Files

Now we have gotten our Flask application running, let’s move on to implementing two-factor authentication (2FA) with TypingDNA. 

We will import the `typingdna.js` file in the page that wants to record our users’ typing patterns. You can download the file from GitHub. You can also get it here and here

Create a `static` folder in your project folder and place the `typingdna.js` file in it. We will also be downloading the TypingDNA `Autocomplete Disabler` and `Typing Visualizer` to clarify to our users that their typing pattern is recorded. 

To do this, download `autocomplete-disabler.js` and `typing-visualizer.js` here and store them in the `static` folder. Our project structure should resemble the image below.

Crypto Wallets Two Factor Authentication Tutorial

Building TypingDNA Enrollment Page

TypingDNA requires us to enroll users so their typing patterns can be matched in the future for authentication. Enrolling a user means we record and save their typing patterns, so the system is familiar with the way a particular user types.

First, we want to update our `login` route to redirect the users to our enrollment page if they have not secured their account with TypingDNA or the verification page if the user has set up TypingDNA authentication on their profile. We will create a variable in our session to store whether TypingDNA has verified TypingDNA has verified the current user session. Update your `login` route in your `app.py` file with the code below:

@app.route("/auth/login/", methods=["GET", "POST"])
def login():
    if "user" in session:
        return redirect(url_for("dashboard"))

    if request.method == "POST":
        username = request.form.get("username").strip().lower()
        password = request.form.get("password")

        user = User.query.filter_by(
            username=username, password=encrypt_password(password)).first()
        if user:
            session["user"] = {
                "email": user.email,
                "username": user.username,
                "address": user.address
            }
            session["typingdna_auth"] = False
            return redirect(url_for("dashboard"))
        else:
            flash("You have supplied invalid login credentials", "danger")
            return redirect(url_for("login"))
    return render_template("login.html")

Next, we will be building the route to enroll our users. Add the following code to your `app.py` file.

@app.route("/auth/typingdna/enroll/", methods=["GET", "POST"])
@login_required
def enroll_typingdna():
    return render_template("typingdna-enroll.html")

We will now create a file named `typingdna-enroll.html` stored in the `templates` folder. Paste the following code in the file:

{% 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>Online TRON Cryptocurrency Wallet</h2>
        <p>Set TypingDNA 2FA on your online wallet</p>
      </div>
    </div>
    <div class="col-lg-6 text-center">
      <div class="alert alert-success" role="alert">
        <h4 class="alert-heading">TypingDNA 2FA Authentication</h4>
        <hr>
        <p class="mb-0">Enter the text below without quotes to complete the authentication process</p>
      </div>
      {% 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 %}
      <div id="failed-auth" class="alert alert-danger" role="alert" style="display: none">
        <strong>You have not completed your authentication, please type the text above</strong>
      </div>
      <form method="POST">
        <div class="form-group">
          <label><strong>"I am authenticated by the way I type"</strong></label>
          <input type="text" class="form-control disable-autocomplete" id="auth-text">
        </div>
        <div class="text-center">
          <input type="hidden" id="tp" name="tp">
          <button type="button" class="btn btn-success" onclick="startAuthentication()">Start Authentication</button>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}

We will now be importing the `typingdna.js`, `autocomplete-disabler.js` and `typing-visualizer.js` files we downloaded earlier in our enrollment page. Add the following code right before the `{% endblock %}` section at the end of the `typingdna-enroll.html` file:

<script src="{{ url_for('static', filename='typingdna.js') }}">
</script>
<script src="{{ url_for('static', filename='autocomplete-disabler.js') }}">
</script>
<script src="{{ url_for('static', filename='typing-visualizer.js') }}">
</script>

Our project structure and enrollment HTML page should resemble the images below.

Crypto Wallets 2FA Tutorial with TypingDNA Authentication API
Secure your Crypto Wallets with Biometric 2FA
Secure your Crypto Wallet with Biometric 2FA

You should also note that the TypingDNA visualizer only captures input elements with the `disable-autocomplete` class attribute. Hence, why we added it to our `input` HTML tag like below.

<input type="text" class="form-control disable-autocomplete" id="auth-text">

Recording Typing Patterns with TypingDNA

Now that we have a page for capturing user typing patterns, we want to integrate the TypingDNA recorder and visualizer into the page to enroll our users. 

First, we will create an instance of the TypingDNA and AutocompleteDisabler classes, so the user typing starts being recorded (as a history of keystroke events).

Add the code below right after the TypingDNA importation in the `typingdna-enroll.html` page.

<script>
  var tdna = new TypingDNA();
  var autocompleteDisabler = new AutocompleteDisabler({
    showTypingVisualizer: true,
    showTDNALogo: true
  });
  TypingDNA.addTarget("auth-text");
  TypingDNA.start();
</script>

Next, we will create a variable to store our users’ captured typing patterns and a function named `startAuthentication` that the users will trigger after entering the auth text.

<script>
  var tdna = new TypingDNA();
  var autocompleteDisabler = new AutocompleteDisabler({
    showTypingVisualizer: true,
    showTDNALogo: true
  });
  TypingDNA.addTarget("auth-text");
  TypingDNA.start();

  var typingPatterns = [];

  function compareTexts(t1, t2) {
    var dt1 = t1.split(' ');
    var dt2 = t2.split(' ');
    var total2 = 0;
    var total1 = 0;
    for (var i in dt2) {
      total2 += (dt1.indexOf(dt2[i]) > -1) ? 1 : 0;
    }
    for (var i in dt1) {
      total1 += (dt2.indexOf(dt1[i]) > -1) ? 1 : 0;
    }
    var total = (dt1.length > dt2.length) ? dt1.length : dt2.length;
    var length = (dt1.length > dt2.length) ? dt1.length : dt2.length;
    return total / length;
  }

  function startAuthentication() {
    let typedText = document.getElementById("auth-text").value;
    let textToType = "I am authenticated by the way I type";

    document.getElementById("failed-auth").style.display = "none";
    document.getElementById("auth-text").value = "";
    TypingDNA.stop();

    let typingPattern = tdna.getTypingPattern({
      type: 1,
      text: textToType
    });

    if (typingPattern == null || compareTexts(textToType, typedText) < 0.8) {
      document.getElementById("failed-auth").style.display = "block";
    } else {
      typingPatterns.push(typingPattern);

      if (typingPatterns.length == 3) {
        let tp = typingPatterns[0] + ";" + typingPatterns[1] + ";" + typingPatterns[2];
        document.getElementById("tp").value = tp;
        document.forms[0].submit();
      } else {
        alert("Successfully logged typing pattern, please type the text again to improve accuracy");
      }
    }

    TypingDNA.reset();
    TypingDNA.start();
  }
</script>

In the code above, when the `startAuthentication` function is called, we first told TypingDNA to stop recording user keystrokes using the `TypingDNA.stop()` method so we could analyze them.

Then, we captured the user typing pattern with the `sametext` TypingDNA capture method. You can find more ways in the official docs.

After capturing the typing pattern, we need to check to make sure the process was successful. If it wasn’t, we’ll display an error message for the user.If it was successful, we store it in the `typingPatterns` variable we created earlier.

To check if the typing pattern was captured successfully, we performed a few checks on the user input following the TypingDNA docs guidelines. 

First, we checked if the pattern captured is null (this means the user did not type the correct text). Then we accounted for typos the user can make when typing by checking if they typed at least 80% of the text that should be typed using `(typedText.length / textToType.length) < 0.8` and checking if at least 80% of the words were typed correctly using `compareTexts(textToType, typedText) < 0.8`.

For accurate recording and matching of user typing patterns, we require the users to type the authentication text three times. In our code above, we check if we have captured up to three patterns. If we have, we submit the form with `document.forms[0].submit()`; else, we store the pattern and ask the user to start all over. 

Before submitting the typing patterns, we concatenate them into a single string separated by semicolons (;) as required by TypingDNA Authentication API.

Biometric 2FA tutorial for your Crypto Wallet

After saving the typing pattern and telling the user to start all over, we use the `TypingDNA.reset()` method to reset everything that has been recorded and `TypingDNA.start()` to start the recorder again because we stopped it earlier.

Saving Typing Patterns with TypingDNA

Saving typing patterns with TypingDNA means storing the captured patterns in our account so TypingDNA can analyze and use them for future authentications. We will be using the ”auto” endpoint in the TypingDNA Authentication API to record and verify typing patterns captured from our users.

To interact with the TypingDNA Authentication API, we will be creating a helper library to make things easy for us. Create a file named `typingdna.py` in your project folder and save the following code in it:

import base64
import hashlib
import requests


class TypingDNA:
    def __init__(self, apiKey, apiSecret):
        self.apiKey = apiKey
        self.apiSecret = apiSecret
        self.base_url = "https://api.typingdna.com"

        authstring = f"{apiKey}:{apiSecret}"
        self.headers = {
            "Authorization": "Basic " + base64.encodebytes(authstring.encode()).decode().replace("\n", ""),
            "Content-Type": "application/x-www-form-urlencoded"
        }

    def auto(self, id, tp, custom_field=None):
        url = f"{self.base_url}/auto/{id}"
        data = {
            "tp": tp,
            "custom_field": custom_field
        }
        return requests.post(url, headers=self.headers, data=data)

    def check_user(self, id, pattern_type=None, text_id=None, custom_field=None):
        url = f"{self.base_url}/user/{id}"
        params = {
            "type": pattern_type,
            "text_id": text_id,
            "custom_field": custom_field
        }
        return requests.get(url, headers=self.headers, params=params)

    def hash_text(self, text):
        reversed_text = text[::-1]
        text_to_hash = text + reversed_text
        return hashlib.sha512(text_to_hash.encode()).hexdigest()

Our project structure should resemble the image below:

We also added a function to hash users’ IDs before submitting them to TypingDNA Authentication API as defined in the official docs.

Now that we have built our helper library, let’s move on to saving our users’ typing patterns in our accounts for future matching. 

First, import the helper library class into our Flask app. We will be creating an instance of our helper class by supplying the `apiKey` and `apiSecret` we saved from our TypingDNA dashboard earlier.

from typingdna import TypingDNA
tdna = TypingDNA("apiKey", "apiSecret")

Our `app.py` file libraries importation should look like the image below:

Next, we want to update our `enroll_typingdna` route to save the typing patterns we received in our TypingDNA account. Update the route with the code below:

@app.route("/auth/typingdna/enroll/", methods=["GET", "POST"])
@login_required
def enroll_typingdna():
    if request.method == "POST":
        tp = request.form.get("tp")
        username = session["user"]["username"]
        r = tdna.auto(tdna.hash_text(username), tp)
        if r.status_code == 200:
            session["typingdna_auth"] = True
            flash("You have successfully registered TypingDNA 2FA", "success")
            return redirect(url_for("dashboard"))
        else:
            flash(r.json()["message"], "danger")
            return redirect(url_for("enroll_typingdna"))
    return render_template("typingdna-enroll.html")

In the code above, we retrieved the user’s typing pattern via the form then sent it to the TypingDNA Authentication API via the “auto” endpoint. If the status code of the request was successful (status code 200), we will update the user data in our database to indicate they have been enrolled, and mark the current logged-in session as authenticated with `session[“typingdna_auth”] = True`. 

After this, we redirect the user to their dashboard. If the authentication was unsuccessful (due to an error from TypingDNA), we will prompt the error message to the user and let them retry the enrollment process.

Authenticating Users with TypingDNA

Adding 2FA to Our Login

The whole point of two-factor authentication (2FA) requires users to pass two different authentication processes before granting them access to the system. We want to make sure the user passes our TypingDNA test right after they have logged in (for already enrolled users). 

We will be making use of the “Check User” endpoint to verify the enrollment status of our users by checking the number of typing patterns we have collected from them and will be redirecting users who have not been enrolled to the enrollment page and after that, redirect users who have not verified their TypingDNA identity to the verification page. Add the code below to the top of the `dashboard` route we created earlier:

def check_typingdna(user, pattern_type):
    r = tdna.check_user(tdna.hash_text(user.username), pattern_type=pattern_type)
    if r.status_code == 200:
        data = r.json()
        if data["count"] >= 3:
            return True
        else:
            return False
    else:
        abort(500)


@app.route("/dashboard/", methods=["GET", "POST"])
@login_required
def dashboard():
    user = User.query.filter_by(username=session["user"]["username"]).first()
    pattern_type = 1
    if not check_typingdna(user, pattern_type):
        return redirect(url_for("enroll_typingdna"))
    if not session["typingdna_auth"]:
        return redirect(url_for("verify_typingdna"))


@app.errorhandler(500)
def internal_server_error_handler(e):
    return "<h1>An internal server error occured or our servers have exceeded TypingDNA API rate limits.</h1>"

Building TypingDNA Verification Page

First, we will create a `verify_typingdna` route that will capture user typing patterns and, afterward, use TypingDNA Authentication API to verify the claimed user’s identity. Add the following code to your `app.py` file.

@app.route("/auth/typingdna/verify/", methods=["GET", "POST"])
@login_required
def verify_typingdna():
    return render_template("typingdna-verify.html")

After that, we will create a file named `typingdna-verify.html` that will be stored in the `templates` folder. Paste the following code in the file:

{% 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>Online TRON Cryptocurrency Wallet</h2>
        <p>Verify TypingDNA 2FA to access your online wallet</p>
      </div>
    </div>
    <div class="col-lg-6 text-center">
      <div class="alert alert-success" role="alert">
        <h4 class="alert-heading">TypingDNA 2FA Authentication</h4>
        <hr>
        <p class="mb-0">Enter the text above without quotes to complete the authentication process</p>
      </div>
      {% 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 %}
      <div id="failed-auth" class="alert alert-danger" role="alert" style="display: none">
        <strong>You have not completed your authentication, please type the text above</strong>
      </div>
      <form method="POST">
        <div class="form-group">
          <label><strong>"I am authenticated by the way I type"</strong></label>
          <input type="text" class="form-control disable-autocomplete" id="auth-text">
        </div>
        <div class="text-center">
          <input type="hidden" id="tp" name="tp">
          <button type="button" class="btn btn-success" onclick="startAuthentication()">Start Authentication</button>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}

Next, we will be importing the `typingdna.js`, `autocomplete-disabler.js` and `typing-visualizer.js` files we downloaded earlier in our verification page. Add the following code right before the `{% endblock %}` section at the end of the `typingdna-verify.html` file:

<script src="{{ url_for('static', filename='typingdna.js') }}">
</script>
<script src="{{ url_for('static', filename='autocomplete-disabler.js') }}">
</script>
<script src="{{ url_for('static', filename='typing-visualizer.js') }}">
</script>

Verifying User Identities with TypingDNA

We now have a page for verifying our users’ identity; we want to integrate the TypingDNA recorder into the page, to capture, analyze, and match user typing patterns. First, we will create an instance of the TypingDNA and AutocompleteDisabler classes so the user typing starts being recorded (as a history of keystroke events).

Add the code below right after the TypingDNA importation in the `typingdna-enroll.html` page.

<script>
  var tdna = new TypingDNA();
  var autocompleteDisabler = new AutocompleteDisabler({
    showTypingVisualizer: true,
    showTDNALogo: true
  });
  TypingDNA.addTarget("auth-text");
  TypingDNA.start();

  function compareTexts(t1, t2) {
    var dt1 = t1.split(' ');
    var dt2 = t2.split(' ');
    var total2 = 0;
    var total1 = 0;
    for (var i in dt2) {
      total2 += (dt1.indexOf(dt2[i]) > -1) ? 1 : 0;
    }
    for (var i in dt1) {
      total1 += (dt2.indexOf(dt1[i]) > -1) ? 1 : 0;
    }
    var total = (dt1.length > dt2.length) ? dt1.length : dt2.length;
    var length = (dt1.length > dt2.length) ? dt1.length : dt2.length;
    return total / length;
  }

  function startAuthentication() {
    let typedText = document.getElementById("auth-text").value;
    let textToType = "I am authenticated by the way I type";

    document.getElementById("failed-auth").style.display = "none";
    document.getElementById("auth-text").value = "";
    TypingDNA.stop();

    let typingPattern = tdna.getTypingPattern({
      type: 1,
      text: textToType
    });

    if (typingPattern == null || compareTexts(textToType, typedText) < 0.8) {
      document.getElementById("failed-auth").style.display = "block";
      TypingDNA.reset();
      TypingDNA.start();
    } else {
      document.getElementById("tp").value = typingPattern;
      document.forms[0].submit();
    }
  }
</script>

In the code above, when the `startAuthentication` function is called, we first stopped TypingDNA from recording keystrokes, then we captured the user typing pattern with the `sametext` capture method. If the capturing is successful, we will submit the form with the recorded typing pattern for verification.

We will also update our `verify_typingdna` route to capture and verify the submitted user typing pattern with the `auto` endpoint in the TypingDNA Authentication API.

@app.route("/auth/typingdna/verify/", methods=["GET", "POST"])
@login_required
def verify_typingdna():
    if request.method == "POST":
        tp = request.form.get("tp")
        username = session["user"]["username"]
        r = tdna.auto(tdna.hash_text(username), tp)
        if r.status_code == 200:
            if r.json()["result"] == 1:
                session["typingdna_auth"] = True
                return redirect(url_for("dashboard"))
            else:
                flash(
                    "You failed the TypingDNA verification check, please try again", "danger")
                return redirect(url_for("verify_typingdna"))
        else:
            flash(r.json()["message"], "danger")
            return redirect(url_for("verify_typingdna"))
    return render_template("typingdna-verify.html")

If the request was successful (status code 200), we check if the typing pattern submitted is an accurate match for our user profile. If it is, we verify the current user session with `session[“typingdna_auth”] = True`. If there was an error or not a match, we will prompt the user’s error message and retry the authentication process.

Implementing 2FA Fallback Option with Email OTP

Let’s build a fallback option used as user authentication for backing up TypingDNA Authentication API. The fallback option is vital when the user’s typing pattern is severely damaged (e.g., the user breaks their hand) or conditions that prevent TypingDNA from accurately matching their typing pattern.

Update the verification form in our `typingdna-verify.html` file with the code below

<form method="POST">
  <div class="form-group">
    <label><strong>"I am authenticated by the way I type"</strong></label>
    <input type="text" class="form-control disable-autocomplete" id="auth-text">
  </div>
  <div class="text-center">
    <input type="hidden" id="tp" name="tp">
    <button type="button" class="btn btn-success" onclick="startAuthentication()">Start Authentication</button>
  </div>
  <div class="text-center">
    <a href="{{ url_for('otp_verification') }}">
      <p class="mt-2">Can't complete TypingDNA verification? Use email OTP instead</p>
    </a>
  </div>
</form>
Secure your Cryptocurrency Wallet with TypingDNA

Next, we need to install the required libraries required for sending emails with Flask. Run the following code in your terminal:

pip install Flask-Mail

After installing the `Flask-Mail` module, we need to integrate it into our application. Add the following code to your `app.py` file:

import random
from flask_mail import Mail, Message

app.config["MAIL_SUPPRESS_SEND"] = True
mail = Mail(app)

For our demo, we will be using the `MAIL_SUPPRESS_SEND` setting in Flask, so it simulates sending emails without the need for an SMTP server; you can read more on Flask-Mail here. Our Flask application imports should now resemble the image below:

We will also be adding a route in our Flask application responsible for generating and verifying OTPs sent to users’ emails.

@app.route("/auth/email/verify/", methods=["GET", "POST"])
@login_required
def otp_verification():
    if request.method == "POST":
        otp_code = request.form.get("otp_code").strip()
        if encrypt_password(otp_code) == session["otp_code"]:
            session["typingdna_auth"] = True
            session.pop("otp_code")
            return redirect(url_for("dashboard"))
        else:
            flash(
                "You failed the Email OTP verification check, please try again", "danger")
            return redirect(url_for("otp_verification"))

    otp_code = str(random.randint(100000, 999999))
    session["otp_code"] = encrypt_password(otp_code)
    msg = Message("Crypto Wallet OTP", sender="otp@flaskcryptowallet.com",
                  recipients=[session["user"]["email"]])
    msg.body = f"The generated OTP for your Flask Cryptocurrency Wallet is {otp_code}"
    print(msg)
    mail.send(msg)

    flash("Check your email address for the verification OTP", "success")
    return render_template("email-verify.html")

Next, we will be creating the `email-verify.html` file that this route will render. The page would have a form that the user will use to submit the received OTP.

Create a file named `email-verify.html` in our `templates` folder and save the following code in it:

{% 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>Online TRON Cryptocurrency Wallet</h2>
        <p>Verify Email OTP to access your online wallet</p>
      </div>
    </div>

    <div class="col-lg-6 mt-2">
      {% 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_code">Email OTP</label>
          <input type="number" class="form-control" id="otp_code" name="otp_code" placeholder="Enter the Email OTP Received" required>
        </div>
        <div class="text-center">
          <button type="submit" class="btn btn-success">Verify Email OTP</button>
        </div>
        <div class="text-center">
          <a href="{{ url_for('verify_typingdna') }}">
            <p class="mt-2">Can't complete email OTP? Use TypingDNA verification instead</p>
          </a>
        </div>
      </form>
    </div>
  </div>
</div>
{% endblock %}

We have now set up our email OTP verification functionality to serve as our 2FA fallback option. Accessing the route should give you a response similar to the image below:

Securing User Actions with TypingDNA

We also want to implement risk-based authentication (RBA) for our wallet withdrawal functionality with TypingDNA to serve as an extra layer of security for the action in our application. Update the withdrawal form in the `dashboard.html` file with the code below:

<div id="failed-auth" class="alert alert-danger" role="alert" style="display: none">
  <strong>You have not completed your authentication, please type the text above</strong>
</div>
<form class="col-lg-6 col-md-8 offset-lg-3 offset-md-2 text-center" method="POST">
  <div class="form-group">
    <label for="wallet-balance">Wallet Balance</label>
    <input type="text" id="wallet-balance" class="form-control" value="{{ '{:,.6f}'.format(balance) }} TRX" readonly>
  </div>
  <div class="form-group">
    <label for="amount">Withdrawal Amount</label>
    <input type="number" step="0.000001" name="amount" class="form-control" id="amount" placeholder="Enter withdrawal amount" required>
  </div>
  <div class="form-group">
    <label for="address">Withdrawal Address</label>
    <input type="text" class="form-control" id="address" name="address" placeholder="Enter withdrawal address" required>
  </div>
  <div class="form-group">
    <label for="password">Account Password</label>
    <input type="password" class="form-control" id="password" name="password" placeholder="Enter Password" required>
  </div>
  <div class="form-group">
    <label>Enter the text <strong>I am authenticated by the way I type</strong> to complete the authentication process</label>
    <input type="hidden" id="tp" name="tp">
    <input type="text" class="form-control disable-autocomplete" id="auth-text">
  </div>
  <button type="button" class="btn btn-success" onclick="startAuthentication()">Make Withdrawal</button>
</form>

Next, we want to add the TypingDNA recorder and visualizer to the `dashboard.html` file to capture user typing patterns and submit the withdrawal form.

<script src="{{ url_for('static', filename='typingdna.js') }}">
</script>
<script src="{{ url_for('static', filename='autocomplete-disabler.js') }}">
</script>
<script src="{{ url_for('static', filename='typing-visualizer.js') }}">
</script>
<script>
  var tdna = new TypingDNA();
  var autocompleteDisabler = new AutocompleteDisabler({
    showTypingVisualizer: true,
    showTDNALogo: true
  });
  TypingDNA.addTarget("auth-text");
  TypingDNA.start();

  function compareTexts(t1, t2) {
    var dt1 = t1.split(' ');
    var dt2 = t2.split(' ');
    var total2 = 0;
    var total1 = 0;
    for (var i in dt2) {
      total2 += (dt1.indexOf(dt2[i]) > -1) ? 1 : 0;
    }
    for (var i in dt1) {
      total1 += (dt2.indexOf(dt1[i]) > -1) ? 1 : 0;
    }
    var total = (dt1.length > dt2.length) ? dt1.length : dt2.length;
    var length = (dt1.length > dt2.length) ? dt1.length : dt2.length;
    return total / length;
  }

  function startAuthentication() {
    let typedText = document.getElementById("auth-text").value;
    let textToType = "I am authenticated by the way I type";

    document.getElementById("failed-auth").style.display = "none";
    document.getElementById("auth-text").value = "";
    TypingDNA.stop();

    let typingPattern = tdna.getTypingPattern({
      type: 1,
      text: textToType
    });

    if (typingPattern == null || compareTexts(textToType, typedText) < 0.8) {
      document.getElementById("failed-auth").style.display = "block";
      TypingDNA.reset();
      TypingDNA.start();
    } else {
      document.getElementById("tp").value = typingPattern;
      document.forms[1].submit();
    }
  }
</script>

Finally, we will verify the typing pattern in our withdrawal function before processing the request. Add the following code to the `dashboard` route in the `app.py` file:

tp = request.form.get("tp")
username = session["user"]["username"]
r = tdna.auto(tdna.hash_text(username), tp)
if r.status_code == 200:
    if r.json()["result"] == 0:
        flash(
            "You failed the TypingDNA verification check, please try again", "danger")
        return redirect(url_for("dashboard"))
else:
    flash(r.json()["message"], "danger")
    return redirect(url_for("dashboard"))

Conclusion

By integrating TypingDNA Authentication API with Python and Flask, we were able to implement two-factor authentication and risk-based authentication using biometrics in a cryptocurrency wallet with minimal effort. We also saw how easy it was to create and verify identities by analyzing user typing patterns with TypingDNA.

The source code of our application is available on GitHub. Trying out TypingDNA for biometric authentication was very interesting, and I can’t wait to see the amazing things you build with it!

If you have any questions, don’t hesitate to contact me on Twitter: @LordGhostX

Share: