Laravel 10 - Two Factor Authentication With SMS example

25-May-2023

.

Admin

Laravel 10 - Two Factor Authentication With SMS example

Hi Friends,

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

We will use Mobile OTP authentication with SMS Login.

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

Laravel provides a variety of authentication features out of the box.

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

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

Step 1: Download Laravel


Let us begin the tutorial by installing a new Laravel application. if you have already created the project, then skip the following step.

composer create-project laravel/laravel example-app

Step 2: Install and configure Twilio library

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

composer require twilio/sdk

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

After creating a Twilio account, add Twilio credentials to the .env file in the root directory.

.env

TWILIO_SID=twilio_sid

TWILIO_TOKEN=twilio_token

TWILIO_FROM=number_here

Step 3: Database Configuration

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

.env

DB_CONNECTION=mysql

DB_HOST=127.0.0.1

DB_PORT=3306

DB_DATABASE=2fa_authentication

DB_USERNAME=root

DB_PASSWORD=secret

Step 4: Add Migration

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

php artisan make:migration create_user_codes_table

The command will create a migration class in the database/migrations directory.

database/migrations/create_user_codes_table.php

<?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::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 the phone field.

database/migrations/create_users_table.php

<?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::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 are done, run the migrate command to generate tables in the database.

php artisan migrate

Step 5 : Add UserCode Model and Update User Model

We will need to create a UserCode model using the below command.

php artisan make:model UserCode

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

app/Models/UserCode.php

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

/**

* The attributes that are mass assignable.

*

* @var array

*/

protected $fillable = [

'user_id',

'code',

];

}

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

app/Models/User.php

<?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: Add Authentication Scaffold

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

composer require laravel/ui

And render authentication views using the following command.

php artisan ui bootstrap --auth

Run the following npm command to compile the assets.

npm install && npm run dev

Step 7: Add Middleware Class

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

php artisan make:middleware TwoFactorAuth

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

app/Http/Middleware/TwoFactorAuth.php

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

app/Http/Kernel.php

<?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: Add Routes

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

routes/web.php

<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\HomeController;

use App\Http\Controllers\TwoFactorAuthController;

/*

|--------------------------------------------------------------------------

| 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!

|

*/

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: Add and Update Controller

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

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

app/Http/Controllers/Auth/RegisterController.php

<?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 the login method.

app/Http/Controllers/Auth/LoginController.php

<?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 the following command.

php artisan make:controller TwoFactorAuthController

Now open the controller file and add the following class methods.

app/Http/Controllers/TwoFactorAuthController.php

<?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: Add and Update Blade Files

This is the last step for coding. In this step, we will update the 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.

resources/views/auth/register.blade.php

@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 a 2fa.blade.php file and add below HTML code into it.

resources/view/2fa.blade.php

@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

Run Laravel App:

All steps have been done, now you have to type the given command and hit enter to run the Laravel app:

php artisan serve

Now, you have to open the web browser, type the given URL and view the app output:

http://localhost:8000/register

I hope it helps you...

#Laravel 10