DEV Community

Cover image for Laravel 9 2FA - Two Factor Authentication with Authy
codeanddeploy
codeanddeploy

Posted on

Laravel 9 2FA - Two Factor Authentication with Authy

Originally posted @ https://codeanddeploy.com visit and download the sample code:
https://codeanddeploy.com/blog/laravel/laravel-9-2fa-two-factor-authentication-with-authy

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-9-2fa-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
Enter fullscreen mode Exit fullscreen mode

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')
]
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Auth via composer

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

"authy/php": "^4.0"
Enter fullscreen mode Exit fullscreen mode

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"
},
Enter fullscreen mode Exit fullscreen mode

then run the following command below:

composer update
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

Then once done. Kindly run the following command:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

Step 6: Setup Profile Controller and Routes

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

php artisan make:controller ProfileController
Enter fullscreen mode Exit fullscreen mode

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.'));
    }
}
Enter fullscreen mode Exit fullscreen mode

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

laravel

laravel

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');
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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-9-2fa

<?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!'));
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

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

php artisan make:controller TwoFactorController
Enter fullscreen mode Exit fullscreen mode

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'));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

php artisan make:request TwoFactorVerifyRequest
Enter fullscreen mode Exit fullscreen mode

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']
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
    });
});
Enter fullscreen mode Exit fullscreen mode

Thank you for reading Laravel 9 Two Factor Authentication. I hope this tutorial can help you. Kindly visit here https://codeanddeploy.com/blog/laravel/laravel-9-2fa-two-factor-authentication-with-authy if you want to download this code.

Happy coding :)

Discussion (0)