How To Add OTP Verification to Laravel 11 Authentication (Laravel Breeze)

How To Add OTP Verification to Laravel 11 Authentication (Laravel Breeze)

In today’s digital landscape, securing user authentication is more critical than ever. While Laravel Breeze offers a lightweight and efficient authentication system, adding an extra layer of security like OTP (One-Time Password) verification can significantly enhance your app’s protection against unauthorized access.

This tutorial will walk you through integrating OTP verification into Laravel 11’s Breeze authentication system. Whether you’re building a new project or improving an existing one, this step-by-step guide will help you implement OTP seamlessly, boosting security and user trust.

Let’s dive in and fortify your Laravel app with OTP authentication!

Step 1: Set Up a Laravel 11 Project

Start by creating a new Laravel project:

composer create-project laravel/laravel laravel-11-otp-breeze

Step 2: Set Database Configuration

Open the .env file and set the database configuration:

.env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your database name(laravel_11_otp)
DB_USERNAME=your database username(root)
DB_PASSWORD=your database password(root)

Step 3: Install Laravel Breeze

Breeze provides basic authentication (login, register, and dashboard).

composer require laravel/breeze --dev
php artisan breeze:install
npm install 
npm run dev
php artisan migrate

image 72 Binaryboxtuts

This will set up authentication routes and views for us.

Step 4: Configure Mail for OTP Delivery

We’ll use Mailtrap (for testing). Open .env and update the mail settings:

MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your-mailtrap-username
MAIL_PASSWORD=your-mailtrap-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@example.com"
MAIL_FROM_NAME="${APP_NAME}"

Replace your-mailtrap-username and your-mailtrap-password with your Mailtrap credentials.

Step 5: Add OTP Fields to the Users Table

Create a migration to add otp and otp_expires_at fields:

php artisan make:migration add_otp_to_users_table

Edit the generated migration file:

database/migrations/xxxx_xx_xx_add_otp_to_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.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('otp')->nullable();
            $table->timestamp('otp_expires_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['otp', 'otp_expires_at']);
        });
    }
};

Run the migration:

php artisan migrate

Step 6: Generate OTP and Send via Email

When a user logs in, we’ll generate an OTP and send it via Laravel Notifications.

Create a notification:

php artisan make:notification OtpNotification

Edit app/Notifications/OtpNotification.php:

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class OtpNotification extends Notification
{
    use Queueable;

    protected $otp;
    /**
     * Create a new notification instance.
     */
    public function __construct($otp)
    {
        $this->otp = $otp;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @return array<int, string>
     */
    public function via(object $notifiable): array
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     */
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Your OTP Code')
            ->line('Your OTP code is: ' . $this->otp)
            ->line('This code will expire in 10 minutes.')
            ->line('If you did not request this, please ignore.');
    }

    /**
     * Get the array representation of the notification.
     *
     * @return array<string, mixed>
     */
    public function toArray(object $notifiable): array
    {
        return [
            //
        ];
    }
}

Step 7: Generate OTP on Login

Modify AuthenticatedSessionController.php to generate and send OTP upon login:

Edit app\Http\Controllers\Auth\AuthenticatedSessionController.php:

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Notifications\OtpNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;

class AuthenticatedSessionController extends Controller
{
    /**
     * Display the login view.
     */
    public function create(): View
    {
        return view('auth.login');
    }

    /**
     * Handle an incoming authentication request.
     */
    public function store(LoginRequest $request): RedirectResponse
    {
        $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);
    
        if (Auth::attempt($request->only('email', 'password'))) {
            $user = Auth::user();
            $otp = rand(100000, 999999);
    
            $user->otp =  $otp;
            $user->otp_expires_at = now()->addMinutes(10);
            $user->save();
    
            $user->notify(new OtpNotification($otp));
    
            return redirect()->route('otp.verify');
        }
    
        return back()->withErrors(['email' => 'Invalid credentials']);
    }

    /**
     * Destroy an authenticated session.
     */
    public function destroy(Request $request): RedirectResponse
    {
        Auth::guard('web')->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return redirect('/');
    }
}

Step 8: Create an OTP Controller

Create a new controller:

php artisan make:controller Auth/OtpController

Edit app/Http/Controllers/Auth/OtpController.php:

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class OtpController extends Controller
{
    public function show()
    {
        return view('auth.otp-verify');
    }

    public function verify(Request $request)
    {
        $request->validate(['otp' => 'required|numeric']);

        $user = Auth::user();

        if ($user && $user->otp === $request->otp && now()->lt($user->otp_expires_at)) {
            $user->otp =  null;
            $user->otp_expires_at = null;
            $user->save();
            return redirect()->route('dashboard')->with('success', 'OTP verified!');
        }

        return back()->withErrors(['otp' => 'Invalid or expired OTP']);
    }
}

Step 9: Create OTP Verification Page

Create a new page resources\views\auth\otp-verify.blade.php and add this line of codes:

resources\views\auth\otp-verify.blade.php

<x-guest-layout>
    <div class="mb-4 text-sm text-gray-600">
        {{ __('Please enter the OTP sent to your email address for verification.') }}
    </div>

    <form method="POST" action="{{ route('otp.verify') }}">
        @csrf

        <div class="mb-4">
            <label for="otp" class="block font-medium text-sm text-gray-700">{{ __('Enter OTP') }}</label>
            <input type="text" name="otp" id="otp" class="block w-full mt-1 rounded-md shadow-sm border-gray-300 focus:ring focus:ring-indigo-200" required autofocus>
            
            @error('otp')
                <span class="text-sm text-red-600">{{ $message }}</span>
            @enderror
        </div>

        <div>
            <x-primary-button>
                {{ __('Verify OTP') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

Step 10: Define Routes

Edit routes/web.php:

&lt;?php
 
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\OtpController;
 
Route::get('/', function () {
    return view('welcome');
});
 
Route::middleware(['auth', 'otp-verified'])->group(function () {
    Route::get('/dashboard', function () {
        return view('dashboard');
    })->name('dashboard');
});
 
Route::middleware(['auth', 'otp-verified'])->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
 

Route::middleware(['auth'])->group(function () {
    Route::get('/otp-verify', [OtpController::class, 'show'])->name('otp.verify');
    Route::post('/otp-verify', [OtpController::class, 'verify']);
});

 
require __DIR__.'/auth.php';

Step 11: Create Middleware

Create middleware:

php artisan make:middleware EnsureOtpVerified

The middleware will force the authenticated user to log out if the OTP expires and when it accesses other pages.

Edit app/Http/Middleware/EnsureOtpVerified.php:

&lt;?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Auth;

class EnsureOtpVerified
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $user = Auth::user();

        if ($user &amp;&amp; $user->otp !== null) {
            if (!$request->is('otp-verify')) {
                Auth::logout();
                return redirect('/login')->withErrors(['otp' => 'Unauthorized access! Please log in again.']);
            }
        }

        if ($user &amp;&amp; $user->otp_expires_at &amp;&amp; now()->greaterThan($user->otp_expires_at)) {
            Auth::logout();
            return redirect('/login')->withErrors(['otp' => 'OTP expired. Please log in again.']);
        }

        return $next($request);
    }
}

Register it in bootstrap/app.php:

&lt;?php


use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\EnsureOtpVerified;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'otp-verified' => EnsureOtpVerified::class
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Step 12: Run the Laravel App

Run this command to start the Laravel App:

php artisan serve

After successfully running your app, open this URL in your browser:

http://localhost:8000

Screenshots:

image 73 Binaryboxtuts
image 74 Binaryboxtuts
image 75 Binaryboxtuts

Leave a Reply

Your email address will not be published. Required fields are marked *