Laravel 9 2FA - Two Factor Authentication with Authy
Code And Deploy
Posted on May 16, 2022
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.
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
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.
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.
<?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');
});
});
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 :)
Posted on May 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.