Laravel 8 - Two Factor Authentication With SMS example

Laravel 8 , Html , Laravel , PHP

Nicesnippets

891

23-11-2021


Laravel 8 - Two Factor Authentication With SMS example

Hi Friends,

In this tutorial article, we will discuss on how to create two factor authentication in Laravel 8.

We will use Mobile OTP authentication with SMS Login.

In this tutorial, we will use Twilio service to send SMS to international mobile number.

Laravel provides varienty of authentication feature out-of-the-box.

Additionally you can also customize authentication flow as per your requirement for user.

Let's start go step by step through tutorial from following steps:

Step 1 : Create new Laravel application


First of all, we need to create fresh Laravel application using Composer command.

So open your terminal OR command prompt and run below command:

composer create-project laravel/laravel authentication --prefer-dist

Then change Terminal working directory to project root.

cd authentication

Step 2 : Install and configure Twilio library

In the second step, we will install twilio/sdk library which provides easy way to send SMS in Laravel application.

composer require twilio/sdk

While the library is installing, let's create Twilio account and get account SID, token and number.

After creating Twilio account, add Twilio credentials at .env file at the root directory.

TWILIO_SID=twilio_sid

TWILIO_TOKEN=twilio_token

TWILIO_FROM=number_here

Step 3 : Configure database in .env file

Now we will need to configure database connection. In the .env file, change below database credentials with your MySQL.

DB_CONNECTION=mysql

DB_HOST=127.0.0.1

DB_PORT=3306

DB_DATABASE=2fa_authentication

DB_USERNAME=root

DB_PASSWORD=secret

Step 4 : Create and run migration file

In the forth step, we will create user_codes migration file using below Artisan command.

php artisan make:migration create_user_codes_table

The command will create migration class at database/migrations directory.

<?php

use Illuminate\Database\Migrations\Migration;

use Illuminate\Database\Schema\Blueprint;

use Illuminate\Support\Facades\Schema;

class CreateUserCodesTable extends Migration

{

/**

* Run the migrations.

*

* @return void

*/

public function up()

{

Schema::create('user_codes', function (Blueprint $table) {

$table->id();

$table->integer('user_id');

$table->string('code');

$table->timestamps();

});

}

/**

* Reverse the migrations.

*

* @return void

*/

public function down()

{

Schema::dropIfExists('user_codes');

}

}

In the users table migration file, add phone field.

<?php

use Illuminate\Database\Migrations\Migration;

use Illuminate\Database\Schema\Blueprint;

use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration

{

/**

* Run the migrations.

*

* @return void

*/

public function up()

{

Schema::create('users', function (Blueprint $table) {

$table->id();

$table->string('name');

$table->string('email')->unique();

$table->string('phone')->nullable();

$table->timestamp('email_verified_at')->nullable();

$table->string('password');

$table->rememberToken();

$table->timestamps();

});

}

/**

* Reverse the migrations.

*

* @return void

*/

public function down()

{

Schema::dropIfExists('users');

}

}

After these changes done, run the migrate command to generate tables into database.

php artisan migrate

Step 5 : Create UserCode model and update User model

We will need to create UserCode model using below command.

php artisan make:model UserCode

Now open the model class at app/Models/UserCode.php file and add $fillable property.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;

class UserCode extends Model

{

use HasFactory;

public $table = "user_codes";

protected $fillable = [

'user_id',

'code',

];

}

In the User model, add following class method to generate and send sms.

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

use Twilio\Rest\Client;

class User extends Authenticatable

{

use HasApiTokens, HasFactory, Notifiable;

/**

* The attributes that are mass assignable.

*

* @var string[]

*/

protected $fillable = [

'name',

'email',

'phone',

'password',

];

/**

* The attributes that should be hidden for serialization.

*

* @var array

*/

protected $hidden = [

'password',

'remember_token',

];

/**

* The attributes that should be cast.

*

* @var array

*/

protected $casts = [

'email_verified_at' => 'datetime',

];

/**

* generate OTP and send sms

*

* @return response()

*/

public function generateCode()

{

$code = rand(100000, 999999);

UserCode::updateOrCreate([

'user_id' => auth()->user()->id,

'code' => $code

]);

$receiverNumber = auth()->user()->phone;

$message = "Your Login OTP code is ". $code;

try {

$account_sid = getenv("TWILIO_SID");

$auth_token = getenv("TWILIO_TOKEN");

$number = getenv("TWILIO_FROM");

$client = new Client($account_sid, $auth_token);

$client->messages->create($receiverNumber, [

'from' => $number,

'body' => $message]);

} catch (\Exception $e) {

//

}

}

}

Step 6 : Create authentication scaffold

Now, we will create Laravel default authentication scaffold using composer command.

composer require laravel/ui

And render authentication views using following command.

php artisan ui bootstrap --auth

Run the following npm command to compile the assets.

npm install && npm run dev

Step 7 : Create middleware class

In this step, we will create a middleware class which will check if user has two factor authentication enabled or not. Run the below Artisan command to generate TwoFactorAuth middleware class at app/Http/Middleware directory

php artisan make:middleware TwoFactorAuth

Now open app/Http/Middleware/TwoFactorAuth.php and add below code into handle() method.

<?php

namespace App\Http\Middleware;

use Closure;

use Session;

use Illuminate\Http\Request;

class TwoFactorAuth

{

/**

* Handle an incoming request.

*

* @param \Illuminate\Http\Request $request

* @param \Closure $next

* @return mixed

*/

public function handle(Request $request, Closure $next)

{

if (!Session::has('user_2fa')) {

return redirect()->route('2fa.index');

}

return $next($request);

}

}

We will also need to register new middleware at app/Http/Kernel.php $routeMiddleware array.

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel

{

/**

* The application's route middleware.

*

* These middleware may be assigned to groups or used individually.

*

* @var array

*/

protected $routeMiddleware = [

....

'2fa' => \App\Http\Middleware\TwoFactorAuth::class,

];

}

Step 8 : Register authentication routes

In this step, we will need to register authentication routes at routes/web.php file.

<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\HomeController;

use App\Http\Controllers\TwoFactorAuthController;

Auth::routes();

Route::get('/home', [HomeController::class, 'index'])->name('home');

Route::get('two-factor-auth', [TwoFactorAuthController::class, 'index'])->name('2fa.index');

Route::post('two-factor-auth', [TwoFactorAuthController::class, 'store'])->name('2fa.store');

Route::get('two-factor-auth/resent', [TwoFactorAuthController::class, 'resend'])->name('2fa.resend');

Step 9 : Create and update controller class

We have already register routes and controller methods. We are adding two factor authentication feature into current authentication flow. So we need to modify RegisterController and LoginController. For 2fa routes, we will create a seperate controller.

First open app/Http/Controllers/Auth/RegisterController.php file and add phone field into user generate array.

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;

use App\Providers\RouteServiceProvider;

use App\Models\User;

use Illuminate\Foundation\Auth\RegistersUsers;

use Illuminate\Support\Facades\Hash;

use Illuminate\Support\Facades\Validator;

class RegisterController extends Controller

{

use RegistersUsers;

/**

* Where to redirect users after registration.

*

* @var string

*/

protected $redirectTo = RouteServiceProvider::HOME;

/**

* Create a new controller instance.

*

* @return void

*/

public function __construct()

{

$this->middleware('guest');

}

/**

* Get a validator for an incoming registration request.

*

* @param array $data

* @return \Illuminate\Contracts\Validation\Validator

*/

protected function validator(array $data)

{

return Validator::make($data, [

'name' => ['required', 'string', 'max:255'],

'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],

'phone' => ['required', 'max:255', 'unique:users'],

'password' => ['required', 'string', 'min:8', 'confirmed'],

]);

}

/**

* Create a new user instance after a valid registration.

*

* @param array $data

* @return \App\Models\User

*/

protected function create(array $data)

{

return User::create([

'name' => $data['name'],

'email' => $data['email'],

'phone' => $data['phone'],

'password' => Hash::make($data['password']),

]);

}

}

In the app/Http/Controllers/Auth/LoginController.php file, modify login method.

<?php

namespace App\Http\Controllers\Auth;

use Auth;

use App\Http\Controllers\Controller;

use App\Providers\RouteServiceProvider;

use Illuminate\Http\Request;

use Illuminate\Foundation\Auth\AuthenticatesUsers;

class LoginController extends Controller

{

use AuthenticatesUsers;

/**

* Where to redirect users after login.

*

* @var string

*/

protected $redirectTo = RouteServiceProvider::HOME;

/**

* Create a new controller instance.

*

* @return void

*/

public function __construct()

{

$this->middleware('guest')->except('logout');

}

/**

* process login

*

* @return response()

*/

public function login(Request $request)

{

$validated = $request->validate([

'email' => 'required',

'password' => 'required',

]);

if (Auth::attempt($validated)) {

auth()->user()->generateCode();

return redirect()->route('2fa.index');

}

return redirect()

->route('login')

->with('error', 'You have entered invalid credentials');

}

}

Now create TwoFactorAuthController controller class using following command.

php artisan make:controller TwoFactorAuthController

Now open controller file and add following class methods.

<?php

namespace App\Http\Controllers;

use App\Models\UserCode;

use Illuminate\Http\Request;

class TwoFactorAuthController extends Controller

{

/**

* index method for 2fa

*

* @return response()

*/

public function index()

{

return view('2fa');

}

/**

* validate sms

*

* @return response()

*/

public function store(Request $request)

{

$validated = $request->validate([

'code' => 'required',

]);

$exists = UserCode::where('user_id', auth()->user()->id)

->where('code', $validated['code'])

->where('updated_at', '>=', now()->subMinutes(5))

->exists();

if ($exists) {

\Session::put('tfa', auth()->user()->id);

return redirect()->route('home');

}

return redirect()

->back()

->with('error', 'You entered wrong OTP code.');

}

/**

* resend otp code

*

* @return response()

*/

public function resend()

{

auth()->user()->generateCode();

return back()

->with('success', 'We have resent OTP on your mobile number.');

}

}

Step 10 : Create and update blade files

This is the last step for coding. In this step, we will update default register code and add new blade views for OTP input.

First start updating resources/views/auth/register.blade.php file. Add phone field into register view.

@extends('layouts.app')

@section('content')

<div class="container">

<div class="row justify-content-center">

<div class="col-md-8">

<div class="card">

<div class="card-header">{{ __('Register') }}</div>

<div class="card-body">

<form method="POST" action="{{ route('register') }}">

@csrf

<div class="form-group row">

<label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>

<div class="col-md-6">

<input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>

@error('name')

<span class="invalid-feedback" role="alert">

<strong>{{ $message }}</strong>

</span>

@enderror

</div>

</div>

<div class="form-group row">

<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

<div class="col-md-6">

<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email">

@error('email')

<span class="invalid-feedback" role="alert">

<strong>{{ $message }}</strong>

</span>

@enderror

</div>

</div>

<div class="form-group row">

<label for="name" class="col-md-4 col-form-label text-md-right">Phone</label>

<div class="col-md-6">

<input id="phone" type="text" class="form-control @error('phone') is-invalid @enderror" name="phone" value="{{ old('phone') }}" required autocomplete="phone" autofocus>

@error('phone')

<span class="invalid-feedback" role="alert">

<strong>{{ $message }}</strong>

</span>

@enderror

</div>

</div>

<div class="form-group row">

<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

<div class="col-md-6">

<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">

@error('password')

<span class="invalid-feedback" role="alert">

<strong>{{ $message }}</strong>

</span>

@enderror

</div>

</div>

<div class="form-group row">

<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

<div class="col-md-6">

<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">

</div>

</div>

<div class="form-group row mb-0">

<div class="col-md-6 offset-md-4">

<button type="submit" class="btn btn-primary">

{{ __('Register') }}

</button>

</div>

</div>

</form>

</div>

</div>

</div>

</div>

</div>

@endsection

We need to create otp input view file. Create 2fa.blade.php file and add below HTML code into it.

@extends('layouts.app')

@section('content')

<div class="container">

<div class="row justify-content-center">

<div class="col-md-8">

<div class="card">

<div class="card-header">2FA Verification</div>

<div class="card-body">

<form method="POST" action="{{ route('2fa.store') }}">

@csrf

<p class="text-center">We sent code to your phone : {{ substr(auth()->user()->phone, 0, 5) . '******' . substr(auth()->user()->phone, -2) }}</p>

@if ($message = Session::get('success'))

<div class="row">

<div class="col-md-12">

<div class="alert alert-success alert-block">

<button type="button" class="close" data-dismiss="alert">×</button>

<strong>{{ $message }}</strong>

</div>

</div>

</div>

@endif

@if ($message = Session::get('error'))

<div class="row">

<div class="col-md-12">

<div class="alert alert-danger alert-block">

<button type="button" class="close" data-dismiss="alert">×</button>

<strong>{{ $message }}</strong>

</div>

</div>

</div>

@endif

<div class="form-group row">

<label for="code" class="col-md-4 col-form-label text-md-right">Code</label>

<div class="col-md-6">

<input id="code" type="number" class="form-control @error('code') is-invalid @enderror" name="code" value="{{ old('code') }}" required autocomplete="code" autofocus>

@error('code')

<span class="invalid-feedback" role="alert">

<strong>{{ $message }}</strong>

</span>

@enderror

</div>

</div>

<div class="form-group row mb-0">

<div class="col-md-8 offset-md-4">

<a class="btn btn-link" href="{{ route('2fa.resend') }}">Resend Code?</a>

</div>

</div>

<div class="form-group row mb-0">

<div class="col-md-8 offset-md-4">

<button type="submit" class="btn btn-primary">

Submit

</button>

</div>

</div>

</form>

</div>

</div>

</div>

</div>

</div>

@endsection

Step 11 : Run and testing application

That's it. We have created the application. Now we are going to test the application. First run the Laravel server using below command into Terminal.

php artisan serve

Run the url http://localhost:8000/register into your favourite browser. Try to register and login new user.

I hope the tutorial helped you on your development.

Thank you for giving time in reading article.

I hope it help you...


Recommended Posts