Contents
- Step 1: Set Up a Laravel 11 Project
- Step 2: Set Database Configuration
- Step 3: Install Laravel Breeze
- Step 4: Configure Mail for OTP Delivery
- Step 5: Add OTP Fields to the Users Table
- Step 6: Generate OTP and Send via Email
- Step 7: Generate OTP on Login
- Step 8: Create an OTP Controller
- Step 9: Create OTP Verification Page
- Step 10: Define Routes
- Step 11: Create Middleware
- Step 12: Run the Laravel App
- Screenshots:
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

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


