25-May-2023
.
Admin
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