Find Us

Address
123 Main Street
New York, NY 10001

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

Two-factor authentication with TypingDNA Authentication API and PHP Laravel

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.

Introduction

2020 marked the largest increase of people working from home and shopping online. But while this transition benefited online retailers and remote workers, it also made it a lucrative period for cyber criminals

More online transactions meant more opportunities to hack credit card data, while more people working remotely opened up new ways for criminals to target both individuals and organizations. 

80% of firms have seen an increase in cyberattacks in 2020 alone. It’s become crucial to authenticate your application’s users beyond the regular username and password phase.

This article will teach you how to use TypingDNA to implement 2 and Multi-Factor Authentication in your Laravel Project giving your application an extra method of authentication — making it more difficult for cyber criminals to impersonate your users.

Implement Laravel 2Factor Authentication

Implementing TypingDNA with Laravel is easy. Just follow these 4 simple steps:

  1. Laravel Authentication Setup
  2. TypingDNA Client Setup
  3. Laravel Backend 
  4. Verification Process

Here is a preview of what we will be building, and you can also clone the repository to follow along. 

Laravel 2 Factor Authentication Setup

If you’re just starting out creating applications with Laravel, you need to read through Laravel Tutorial: The ultimate guide 2021 before moving forward.

But if you know what Laravel is and how to get started, let’s run this command to install Laravel.

composer create-project laravel/laravel laravel-typingdna

Navigate into the laravel-typingdna directory and start your development server.

cd laravel-typingdna && php artisan serve

Set up the Database

After installing Laravel, the next thing is to configure and set up your database. Let’s create a database using any of these database clients of your choice and keep a note of the login details.

Now, open the .env file and update the following information.

DB_CONNECTION=mysql
DB_PORT=3306
DB_HOST=127.0.0.1
DB_USERNAME=DB USERNAME HERE
DB_DATABASE=DB NAME HERE
DB_PASSWORD=DB PASSWORD HERE

Set up Laravel 2 factor authentication

Laravel makes it very easy to set up authentications by automatically generating authentication pages and setting up the logic properly with a few simple commands.

With the use of Laravel Breeze, the authentication process is made very easy and less time-consuming.

composer require laravel/breeze --dev
php artisan breeze:install
npm install
npm run dev

After running those commands, your authentication process is good to go. Simply navigate to http://127.0.0.1:8000/register or http://127.0.0.1:8000/login.

Now that we have our normal authentication process adequately configured, let’s move on to configuring and setting up TypingDNA for Multi-Factor authentication.

TypingDNA Client Setup

All you have to do is download the client code and insert the script on the page you want to record the typing patterns.

You also need to create a TypingDNA account to obtain your API Key and Secret, which we will use later in the project.

Setting up the Verification process

In our case, we will place the TypingDNA script on our verify page, which is the page the user sees after a successful login process.

<x-slot name="script_h">
        <script src="https://typingdna.com/scripts/typingdna.js"></script>
       <script src="https://github.com/TypingDNA/autocomplete-disabler/blob/main/autocomplete-disabler.js"> </script>

        <script src="https://github.com/TypingDNA/autocomplete-disabler/blob/main/typing-visualizer.js"></script>
    </x-slot>

Next, we will add the following code, which informs TypingDNA to record the typing pattern from the verifytext input field using the addTarget() method of the new TypingDNA() object.

Also, to disable autocomplete and autofill (in some browsers), and disable copy, cut, and paste from the target inputs, we have added the AutocompleteDisabler library.

The TypingVisualizer library adds a visual representation of the user’s typing on selected inputs.

The importance of installing the above libraries is that TypingDNA can accurately measure the user’s typing patterns as users type into the inputs.

<script>
            const tdna = new TypingDNA();
            tdna.addTarget('verifytext');
            document.getElementById('verifytext').focus();

// Disabling Autocomplete
           var disabler = new AutocompleteDisabler({
                showTypingVisualizer: true,
                showTDNALogo: true
            });
            disabler.disableCopyPaste();
            disabler.disableAutocomplete();



            function validate(form) {
                const error = document.getElementById("error")
                const user = form.verifytext.value.toString().trim();
                if (user === 'I am authenticated by the way I type') {
                    form.textid.value = TypingDNA.getTextId(user);
                    form.typingPattern.value = tdna.getTypingPattern({
                        type: 1,
                        text: user
                    });

                    return true;
                }
                error.innerHTML = "Please type in the word displayed below"
                return false
            }
        </script>

The validate() function generates a unique textid and typingPattern based on the user input using getTextId and getTypingPattern methods of TypingDNA respectively.

Setting the Type property to 1 indicates that we want to use the `Sametext` typing pattern, which is most recommended for passwords, emails, and credit card verifications.

Lastly, our verify form looks like this:

<form method="POST" action="{{ route('verifydna') }}" name="form" onsubmit="return validate(this)">
            @csrf
            <x-input name="textid" type="hidden" value="{{ session('textid') }}" />
            <x-input name="typingPattern" type="hidden" value="{{ session('typingPattern') }}" />
            <x-input type="hidden" name="email" value="{{ session('email') }}" />
            <x-input type="hidden" name="password" value="{{ session('password') }}" />

            <p>Two-Factor Authentication. Please type the text below</p>
            @if(session()->has('error_count'))
            <div>
                <em style="color: red; font-size:small; padding:2em; text-align:center" name="error" id="error">Remaining {{3 - session('error_count')-1}} more times.</em>
            </div>
            @endif
            <div style="margin: 2em;">
                <p style="font-weight: bold; text-align:center">I am authenticated by the way I type</p>
            </div>

            <!-- Email Address -->
            <div>
                <x-label for="Text" :value="__('Text')" />
                <x-input id="verifytext" class="block mt-1 w-full disable-autocomplete" type="text" name="verifytext" required autofocus />
            </div>

            <div class="flex items-center mt-4" style="justify-content: space-between;">
                <x-button class="mr-0">
                    {{ __('Submit') }}
                </x-button>
                <div>
                    <em class="ml-3" style="font-size: 10px;">Having troubles?</em>
                    <x-link href="/send-email" class="ml-3" id="email">
                        {{ __('Send Email') }}
                    </x-link>
                </div>
            </div>
        </form>

Laravel Backend

When a user clicks on the Login button, the request is sent to the AuthenticatedSessionController in app/Http/Controllers/Auth. We will replace the login script with the following codes to redirect the user to a verified page if the user exists.

    /**
     * Handle an incoming authentication request.
     *
     * @param  \App\Http\Requests\Auth\LoginRequest  $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store(LoginRequest $request)
    {
        $user = User::where('email', $request->email)->first();
        if ($user) {
            session()->put('typingPattern', $request->typingPattern);
            session()->put('textid', $request->textid);
            session()->put('email', $request->email);
            session()->put('password', $request->password);
            $request->authenticate();
            $request->session()->regenerate();
            return redirect()->route('verify');
        } else {
            // Send back errors
        }
    }



The code above checks for the user to store the user data in the session object and redirect the user to verify page if the user exists or returns an error page.

Setting up the verification process

We created a new verify.blade.php file inside the resources/view/auth folder. Open it and paste in the following code.

<form method="POST" action="{{ route('verifydna') }}" name="form" onsubmit="return validate(this)">
            @csrf
            <x-input name="textid" type="hidden" value="{{ session('textid') }}" />
            <x-input name="typingPattern" type="hidden" value="{{ session('typingPattern') }}" />
            <x-input type="hidden" name="email" value="{{ session('email') }}" />
            <x-input type="hidden" name="password" value="{{ session('password') }}" />

            <p>Two-Factor Authentication. Please type the text below</p>
            @if(session()->has('error_count'))
            <div>
                <em style="color: red; font-size:small; padding:2em; text-align:center" name="error" id="error">Remaining {{3 - session('error_count')-1}} more times.</em>
            </div>
            @endif
            <div style="margin: 2em;">
                <p style="font-weight: bold; text-align:center">I am authenticated by the way I type</p>
            </div>

            <!-- Email Address -->
            <div>
                <x-label for="Text" :value="__('Text')" />
                <x-input id="verifytext" class="block mt-1 w-full disable-autocomplete" type="text" name="verifytext" required autofocus />
            </div>

            <div class="flex items-center mt-4" style="justify-content: space-between;">
                <x-button class="mr-0">
                    {{ __('Submit') }}
                </x-button>
                <div>
                    <em class="ml-3" style="font-size: 10px;">Having troubles?</em>
                    <x-link href="/send-email" class="ml-3" id="email">
                        {{ __('Send Email') }}
                    </x-link>
                </div>
            </div>
        </form>

The code above creates a form with a dummy text input for verification and some hidden form inputs that set the values of textId, typingPattern, email, and password.

Next, we will submit the form to the server to verify the pattern and log the user in if correct.

When we submit the form, the request goes to the verifyTypingDNA method of the AuthenticatedSessionController for processing.

public function verifyTypingDNA(LoginRequest $request)
    {

        $user = User::where('email', $request->email)->first();
        $check = TypingDNA::getInstance()->checkUser($user);
        if ($check['success'] === 1 && $check['count'] >= 0) {
            $result = TypingDNA::getInstance()->doAuto($user, $request->typingPattern);

            if ($result['status'] > 200) {
                return $this->destroy($request);
            }
            if ((isset($result['high_confidence']) && isset($result['result'])) && ($result['high_confidence'] === 1 && $result['result'] === 1)) {
                session()->forget('typingPattern');
                session()->forget('textid');
                session()->forget('email');
                session()->forget('password');
                session()->forget('error_message');
                $request->authenticate();
                $request->session()->regenerate();
                return redirect(RouteServiceProvider::HOME);
            }
            session()->put('error_message', $result['message']);
            session()->put('error_count', $check['count']);
            return redirect()->route('new-verify');
        } else {
            return $this->destroy($request);
        }
    }

Let’s walk through what the above code does:

  • It retrieves the specific user.
  • Checks if the user has stored patterns using the TypingDNA singleton with desktop devices.
  • Uses the doAuto method of the TypingDNA singleton to enroll a new pattern or verify a user’s typing pattern.
  • Logs in the user, clears the session and redirects to the dashboard.

TypingDNA Singleton Class

You can review the TypingDNA singleton created inside app/Services below:

<?php

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\Http;

class TypingDNA
{
    private static $instance = null;
    private $apiKey = null;
    private $apiSecret = null;
    private $typingdna_url = null;
    private $secret = null;


    private function __construct()
    {
        $this->apiKey = env('TYPINGDNA_API_KEY');
        $this->apiSecret = env('TYPINGDNA_API_SECRET');
        $this->typingdna_url = env('TYPINGDNA_BASE_URL');
        $this->secret = env('TYPINGDNA_SECRET');
    }

    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function checkUser(User $user)
    {
        $userId = $this->generateUserID($user->id, $this->secret);
        $result = $this->check($userId);
        return  $result;
    }

    public function doAuto(User $user, $tp)
    {
        $userId = $this->generateUserID($user->id, $this->secret);


        $typingdna_url_de = urldecode($this->typingdna_url . '/auto/' . $userId);
        $response = Http::withToken(base64_encode("$this->apiKey:$this->apiSecret"), 'Basic')->post($typingdna_url_de, [
            'tp' => $tp
        ]);

        $result = $response->json();
        if ($result['status'] === 429) {
            sleep(1);
            $result = $this->doAuto($user, $tp);
        }
        return $result;
    }

    private function check($userid)
    {

        $typingdna_url = urldecode($this->typingdna_url . '/user/' . $userid);

        $response = Http::withToken(base64_encode("$this->apiKey:$this->apiSecret"), 'Basic')->get($typingdna_url);
        return $response->json();
    }

    private function generateUserID($userid, $privateKey)
    {
        return \md5($userid . $privateKey);
    }
}

Conclusion

And you’re done! This article covered how to implement Laravel 2 factor authentication using typing biometrics with the TypingDNA Authentication API.

You can now use these skills to create more effective and appealing applications.

You can clone the repository here.

Happy coding!

Share: