Table of contents

laravel 2fa

In this post, I will share how to implement Laravel 8, 9 2FA - Two Factor Authentication using Authy we know that the two-factor authentication is an extra layer of our application security in case other people get the user credentials and access the account. With this implementation, it will surely not be easy to access the user account because it needs other verification before can continue to authenticate.

 

Laravel two factor

 

In this tutorial, I will use the Authy app for our Laravel Two Factor authentication and I will show you how to do it in step by step process.

 

Before we can start you need to download my previous tutorial about Laravel 9 authentication so that we shorten our process. But if you have your Laravel authentication already then you can skip it and directly implement the Laravel Two Factor Authentication.

 

Just visit here the previous tutorial about authentication.

 

Now let's start.

 

Step 1. Setup Laravel Two Factor Configuration

Using ENV we will add the following code.

 

AUTHY_KEY=YOUR API KEY HERE

 

Don't forget to create an Authy application.

 

In this configuration, you need to add later the Authy Application API Key.

 

Then once done, kindly add the following array value to your config/services.php.

 

'authy' => [
   'key' => env('AUTHY_KEY')
]

 

Step 2: Install Auth via composer

Add the following line to your composer.json inside require JSON values.

 

"authy/php": "^4.0"

 

It should be like this:

"require": {
        "php": "^8.0.2",
        "guzzlehttp/guzzle": "^7.2",
        "laravel/framework": "^9.2",
        "laravel/sanctum": "^2.14.1",
        "laravel/tinker": "^2.7",
        "authy/php": "^4.0"
},

 

then run the following command below:

composer update

 

Step 3: Add Authy Two Factor Columns to the Users table

Run the following command:

php artisan make:migration add_authy_columns_to_users_table

 

Here is the final code of migration:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->boolean('authy_status')->nullable()->after('password');
            $table->string('authy_id', 25)->after('authy_status');
            $table->string('authy_country_code', 10)->after('authy_id');
            $table->string('authy_phone')->after('authy_country_code');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('authy_status');
            $table->dropColumn('authy_id', 25);
            $table->dropColumn('authy_country_code', 10);
            $table->dropColumn('authy_phone');
        });
    }
};

 

Then once done. Kindly run the following command:

php artisan migrate

 

Step 4: Setup User Model Fillable Values and Two Factor Checking

Here is the final code below:

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The database table used by the model.
     *
     * @var string
     */
    protected $table = 'users';

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'username',
        'password',
        'authy_status',
        'authy_id',
        'authy_country_code',
        'authy_phone'
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
        'authy_id'
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * Always encrypt password when it is updated.
     *
     * @param $value
     * @return string
     */
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }

    /**
     * Check if factor enabled
     * 
     * @return boolean
     */
    public function isTwoFactorEnabled()
    {
        return $this->authy_status == 1 ? true : false;
    }
}

 

Step 5: Add Service Class for Authy

Now let's add a service class for our Authy.php navigate App/Services folder then add TwoFactor Folder. Once done create Authy.php file then add the following code.

<?php

namespace App\Services\TwoFactor;

class Authy {

	/**
     * @var \Authy\AuthyApi
     */
    private $api;

    public function __construct()
    {
        $this->api = new \Authy\AuthyApi(config('services.authy.key'));
    }

    /**
     * @param $email
     * @param $phoneNumber
     * @param $countryCode
     * @return int
     * @throws \Exception
     */
    function register($email, $phoneNumber, $countryCode)
    {
        $user = $this->api->registerUser($email, $phoneNumber, $countryCode);

        return $user;
    }

    /**
     * @param $authyId
     * @return bool
     * @throws \Exception
     */
    public function sendToken($authyId)
    {
        $response = $this->api->requestSms($authyId);
        
        return $response;
    }

    /**
     * @param $authyId
     * @param $token
     * @return bool
     * @throws \Exception Nothing will be thrown here
     */
    public function verifyToken($authyId, $token)
    {
        $response = $this->api->verifyToken($authyId, $token);

        return $response;
    }

    /**
     * @param $authyId
     * @return \Authy\value status
     * @throws \Exception if request to api fails
     */
    public function verifyUserStatus($authyId) {
        $response = $this->api->userStatus($authyId);

        return $response;
    }

}

 

 

Step 6: Setup Profile Controller and Routes

Now let's create a ProfileController by running the following command:

 

php artisan make:controller ProfileController

 

laravel 9 2fa

 

Then add the following code below:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use App\Services\TwoFactor\Authy;

class ProfileController extends Controller
{
    protected $users;
    protected $authy;

    public function __construct(User $users, Authy $authy) 
    {
        $this->users = $users;
        $this->authy = $authy;
    }

    public function index() 
    {
        return view('profile.index');
    }

    public function enableTwoFactor(Request $request) 
    {
        $user = auth()->user();

        $checkUser = User::where('authy_country_code', $request->get('country_code'))
            ->where('authy_phone', $request->get('phone_number'))
            ->first();

        if(is_null($checkUser)) {
            $register = $this->authy->register(
                $user->email, 
                $request->get('phone_number'),
                $request->get('country_code')
            );

            if ($register->ok()) {
                $authyId = $register->id();

                $user->update([
                    'authy_status' => false,
                    'authy_id' => $authyId,
                    'authy_country_code' => $request->get('country_code'),
                    'authy_phone' => $request->get('phone_number')
                ]);
            } else {
                return redirect('profile')->with('authy_errors', $register->errors());
            }

        } else {
            $authyId = $checkUser->authy_id;
        }

        $this->authy->sendToken($authyId);

        return redirect('profile/two-factor/verification');
    }

    public function disableTwoFactor(Request $request) 
    {
        $user = auth()->user();

        $user->update([
            'authy_status' => false
        ]);

        return redirect('profile')
            ->with('success',  __('Two factor authentication has been disabled.'));
    }

    public function getVerifyTwoFactor() 
    {
        return view('profile.verify-two-factor');
    }

    public function postVerifyTwoFactor(Request $request) 
    {
        $user = auth()->user();

        $verfiy = $this->authy->verifyToken($user->authy_id, $request->get('authy_token'));

        if ( $verfiy->ok() ) {
            $user->update(['authy_status' => 1]);

            return redirect('profile')
                ->with('success', __('Two factor authentication has been enabled.'));
        }

        return redirect('profile/two-factor/verification')
            ->with('errors', __('Invalid token. Please try again.'));
    }
}

 

The ProfileController functionality consists of enabling two factor, disabling two factor, and verifying two factor when adding it.

 

laravel 9 two factor

laravel authy two factor

 

Then let's set up the profile routes.

 

Route::group(['middleware' => ['auth']], function() {

    /**
     * Profile Routes
     */
    Route::get('/profile', 'ProfileController@index')
        ->name('profile.index');
    Route::post('/profile/two-factor/enable', 'ProfileController@enableTwoFactor')
        ->name('profile.enableTwoFactor');
    Route::post('/profile/two-factor/disable', 'ProfileController@disableTwoFactor')
        ->name('profile.disableTwoFactor');
    Route::get('/profile/two-factor/verification', 'ProfileController@getVerifyTwoFactor')
        ->name('profile.getVerifyTwoFactor');
    Route::post('/profile/two-factor/verification', 'ProfileController@postVerifyTwoFactor')
        ->name('profile.postVerifyTwoFactor');
});

 

Now, let's set up our navigation for our profile then navigate resources/views/layouts/partials/navbar.blade.php. See the code below:

<header class="p-3 bg-dark text-white">
  <div class="container">
    <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
      <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
        <svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap"><use xlink:href="#bootstrap"/></svg>
      </a>

      <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
        <li><a href="#" class="nav-link px-2 text-secondary">Home</a></li>
        <li><a href="#" class="nav-link px-2 text-white">Features</a></li>
        <li><a href="#" class="nav-link px-2 text-white">Pricing</a></li>
        <li><a href="#" class="nav-link px-2 text-white">FAQs</a></li>
        <li><a href="#" class="nav-link px-2 text-white">About</a></li>
      </ul>

      <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3">
        <input type="search" class="form-control form-control-dark" placeholder="Search..." aria-label="Search">
      </form>

      @auth
        
        <div class="dropdown">
          <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-expanded="false">
            {{auth()->user()->name}}
          </button>
          <ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="dropdownMenuButton2">
            <li><a class="dropdown-item active" href="{{ route('profile.index') }}">Profile</a></li>
            <li><hr class="dropdown-divider"></li>
            <li><a class="dropdown-item" href="{{ route('logout.perform') }}">Logout</a></li>
          </ul>
        </div>
      @endauth

      @guest
        <div class="text-end">
          <a href="{{ route('login.perform') }}" class="btn btn-outline-light me-2">Login</a>
          <a href="{{ route('register.perform') }}" class="btn btn-warning">Sign-up</a>
        </div>
      @endguest
    </div>
  </div>
</header>

 

Step 7: Implementation of Authy Two Factor in our Authentication

Now let's implement Authy Two Factor in our authentication we need to modify our authentication code inside LoginController.php. Here is the modified code inside authenticated() method.

 

Laravel two factor

 

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\TwoFactor\Authy;
use App\Http\Requests\LoginRequest;
use Illuminate\Support\Facades\Auth;
use App\Services\Login\RememberMeExpiration;

class LoginController extends Controller
{
    use RememberMeExpiration;

    protected $authy;

    public function __construct(Authy $authy) 
    {
        $this->authy = $authy;
    }

    /**
     * Display login page.
     * 
     * @return Renderable
     */
    public function show()
    {
        return view('auth.login');
    }

    /**
     * Handle account login request
     * 
     * @param LoginRequest $request
     * 
     * @return \Illuminate\Http\Response
     */
    public function login(LoginRequest $request)
    {
        $credentials = $request->getCredentials();

        if(!Auth::validate($credentials)):
            return redirect()->to('login')
                ->withErrors(trans('auth.failed'));
        endif;

        $user = Auth::getProvider()->retrieveByCredentials($credentials);

        Auth::login($user, $request->get('remember'));

        if($request->get('remember')):
            $this->setRememberMeExpiration($user);
        endif;

        return $this->authenticated($request, $user);
    }

    /**
     * Handle response after user authenticated
     * 
     * @param Request $request
     * @param Auth $user
     * 
     * @return \Illuminate\Http\Response
     */
    protected function authenticated(Request $request, $user) 
    {
        if(!$user->isTwoFactorEnabled()){
            return redirect()->intended();
        }

        $status = $this->authy->verifyUserStatus($user->authy_id);

        if($status->ok() && $status->bodyvar('status')->registered) {
            Auth::logout();

            $request->session()->put('auth.2fa.id', $user->id);

            $sms = $this->authy->sendToken($user->authy_id);

            if($sms->ok()){
                return redirect('/token');
            }
        } else {
             Auth::logout();
            return redirect('login')->with('message', __('Could not confirm Authy status!'));
        }
        
    }
}

 

Now let's create a TwoFactorController for our extra layer authentication. Run the following command to create it.

 

php artisan make:controller TwoFactorController

 

Here is the full source code for TwoFactorController.

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use App\Services\TwoFactor\Authy;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\TwoFactorVerifyRequest;

class TwoFactorController extends Controller
{
	public function __construct(User $users, Authy $authy) 
	{
		$this->users = $users;
		$this->authy = $authy;
	}

    /**
     * Display login page.
     * 
     * @return Renderable
     */
    public function show()
    {
        return view('auth.token');
    }

    public function perform(TwoFactorVerifyRequest $request) 
    {
    	$user = $this->users->find(session('auth.2fa.id'));

        if(!$user){
            return redirect('login');
        }

        $verfiy = $this->authy->verifyToken($user->authy_id, $request->get('authy_token'));

        if($verfiy->ok()){
            Auth::login($user);
            return redirect('/');
        } else {
            return redirect('token')->with('authy_error', __('The token you entered is incorrect'));
        }
    }
}

 

Let's create our Validation Request for our Two Factor. Just run the following command:

php artisan make:request TwoFactorVerifyRequest

 

Then add the following code:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class TwoFactorVerifyRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'authy_token' => ['required', 'digits_between:6,10']
        ];
    }
}

 

Then let's create our view for verifying the Authy Two Factor. Inside resources/views/auth folder create token.blade.php file. Then add the following code.

@extends('layouts.auth-master')

@section('content')
    <form method="post" action="{{ route('token.perform') }}">
        
        <input type="hidden" name="_token" value="{{ csrf_token() }}" />
        <img class="mb-4" src="{!! url('images/bootstrap-logo.svg') !!}" alt="" width="72" height="57">
        
        <h1 class="h3 mb-3 fw-normal">Two Factor Authentication</h1>

        @if(Session::get('authy_error', false))
            <div class="alert alert-warning" role="alert">
                <i class="fa fa-check"></i>
                {{ Session::get('authy_error'); }}
            </div>
        @endif

        <div class="form-group form-floating mb-3">
            <input type="text" class="form-control" name="authy_token" value="{{ old('authy_token') }}" placeholder="Authy Token" required="required" autofocus>
            <label for="floatingName">Authy Token</label>
            @if ($errors->has('authy_token'))
                <span class="text-danger text-left">{{ $errors->first('authy_token') }}</span>
            @endif
        </div>

        <button class="w-100 btn btn-lg btn-primary" type="submit">Verify</button>
        
        @include('auth.partials.copy')
    </form>
@endsection

 

Then let's set up our two-factor routes. See below:

/**
 * Two Factor Routes
 */
Route::get('/token', 'TwoFactorController@show')->name('token.show');
Route::post('/token', 'TwoFactorController@perform')->name('token.perform');

 

Here is the full source code of our routes.

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::group(['namespace' => 'App\Http\Controllers'], function()
{   
    /**
     * Home Routes
     */
    Route::get('/', 'HomeController@index')->name('home.index');

    Route::group(['middleware' => ['guest']], function() {
        /**
         * Register Routes
         */
        Route::get('/register', 'RegisterController@show')->name('register.show');
        Route::post('/register', 'RegisterController@register')->name('register.perform');

        /**
         * Login Routes
         */
        Route::get('/login', 'LoginController@show')->name('login.show');
        Route::post('/login', 'LoginController@login')->name('login.perform');


        /**
         * Two Factor Routes
         */
        Route::get('/token', 'TwoFactorController@show')->name('token.show');
        Route::post('/token', 'TwoFactorController@perform')->name('token.perform');
    });

    Route::group(['middleware' => ['auth']], function() {

        /**
         * Profile Routes
         */
        Route::get('/profile', 'ProfileController@index')
            ->name('profile.index');
        Route::post('/profile/two-factor/enable', 'ProfileController@enableTwoFactor')
            ->name('profile.enableTwoFactor');
        Route::post('/profile/two-factor/disable', 'ProfileController@disableTwoFactor')
            ->name('profile.disableTwoFactor');
        Route::get('/profile/two-factor/verification', 'ProfileController@getVerifyTwoFactor')
            ->name('profile.getVerifyTwoFactor');
        Route::post('/profile/two-factor/verification', 'ProfileController@postVerifyTwoFactor')
            ->name('profile.postVerifyTwoFactor');

        /**
         * Logout Routes
         */
        Route::get('/logout', 'LogoutController@perform')->name('logout.perform');
    });
});

 

Download Source Code

 

Thank you for reading Laravel 9 Two Factor Authentication. I hope it helps :)