When I started building secure web applications, I realized how important it is to provide users with a safe and user-friendly login system. One-time password (OTP) authentication is a great way to enhance security by sending a unique code to the user’s email or phone, which they use to log in.
In this article, I’ll share how I implemented OTP-based login in Laravel, a popular PHP framework. This guide is written in simple language, so even if you’re new to Laravel, you can follow along. Let’s dive into the step-by-step process to set up OTP-based login in your Laravel application!
First, I make sure I have a fresh Laravel project. If you don’t have one, you can create it using Composer. Open your terminal and run:
composer create-project --prefer-dist laravel/laravel otp-login
This creates a new Laravel project named otp-login
. Once the installation is complete, navigate to the project directory and ensure your environment is set up (e.g., database configuration in the .env
file).
I use a MySQL database for this project, but you can use any database supported by Laravel. Update your .env
file with your database credentials:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=otp_login
DB_USERNAME=your_username
DB_PASSWORD=your_password
Run the migration to create the default users
table:
php artisan migrate
Since I’ll send OTPs via email, I configure Laravel’s mail system. For this example, I use Mailtrap, a service for testing emails, but you can use any mail service like SendGrid or SMTP. Update the .env
file with your mail configuration:
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_mailtrap_username
MAIL_PASSWORD=your_mailtrap_password
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="${APP_NAME}"
I need a table to store OTPs temporarily. I create a new model and migration for OTPs by running:
php artisan make:model OTP -m
This generates an OTP
model and a migration file. In the migration file (located in database/migrations
), I define the schema for the otps
table:
Schema::create('otps', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('otp');
$table->timestamp('expires_at');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Run the migration:
php artisan migrate
I update the User
model (app/Models/User.php
) to establish a relationship with the OTP
model:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
public function otps()
{
return $this->hasMany(OTP::class);
}
}
I create a controller to handle OTP generation and verification:
php artisan make:controller Auth/OTPController
In app/Http/Controllers/Auth/OTPController.php
, I add the logic to generate and verify OTPs:
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\OTP;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
class OTPController extends Controller
{
public function showLoginForm()
{
return view('auth.otp-login');
}
public function sendOTP(Request $request)
{
$request->validate(['email' => 'required|email|exists:users,email']);
$user = User::where('email', $request->email)->first();
// Generate a 6-digit OTP
$otp = rand(100000, 999999);
$expiresAt = now()->addMinutes(10);
// Save OTP to database
OTP::create([
'user_id' => $user->id,
'otp' => $otp,
'expires_at' => $expiresAt,
]);
// Send OTP via email
Mail::raw("Your OTP is: $otp", function ($message) use ($user) {
$message->to($user->email)->subject('Your OTP Code');
});
return redirect()->route('otp.verify')->with('email', $user->email);
}
public function showVerifyForm()
{
return view('auth.otp-verify');
}
public function verifyOTP(Request $request)
{
$request->validate([
'email' => 'required|email|exists:users,email',
'otp' => 'required|digits:6',
]);
$user = User::where('email', $request->email)->first();
$otp = OTP::where('user_id', $user->id)
->where('otp', $request->otp)
->where('expires_at', '>=', now())
->first();
if ($otp) {
// Log the user in
auth()->login($user);
$otp->delete(); // Delete used OTP
return redirect()->route('home')->with('success', 'Logged in successfully!');
}
return back()->withErrors(['otp' => 'Invalid or expired OTP']);
}
}
I create two Blade views for the OTP login process.
1. OTP Login Form (resources/views/auth/otp-login.blade.php
):
<!DOCTYPE html>
<html>
<head>
<title>OTP Login</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<h2>OTP Login</h2>
<form method="POST" action="{{ route('otp.send') }}">
@csrf
<div>
<label>Email</label>
<input type="email" name="email" required>
@error('email')
<span>{{ $message }}</span>
@enderror
</div>
<button type="submit">Send OTP</button>
</form>
</div>
</body>
</html>
2. OTP Verification Form (resources/views/auth/otp-verify.blade.php
):
<!DOCTYPE html>
<html>
<head>
<title>Verify OTP</title>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
<h2>Verify OTP</h2>
<form method="POST" action="{{ route('otp.verify') }}">
@csrf
<div>
<label>Email</label>
<input type="email" name="email" value="{{ session('email') }}" readonly>
</div>
<div>
<label>OTP</label>
<input type="text" name="otp" required>
@error('otp')
<span>{{ $message }}</span>
@enderror
</div>
<button type="submit">Verify OTP</button>
</form>
</div>
</body>
</html>
I add routes for OTP login in routes/web.php
:
use App\Http\Controllers\Auth\OTPController;
Route::get('/otp/login', [OTPController::class, 'showLoginForm'])->name('otp.login');
Route::post('/otp/send', [OTPController::class, 'sendOTP'])->name('otp.send');
Route::get('/otp/verify', [OTPController::class, 'showVerifyForm'])->name('otp.verify');
Route::post('/otp/verify', [OTPController::class, 'verifyOTP'])->name('otp.verify');
Route::get('/home', function () {
return view('home');
})->name('home');
I start the Laravel server:
php artisan serve
I visit http://localhost:8000/otp/login
, enter an email, receive an OTP, and verify it to log in. I ensure a user exists in the users
table (you can create one via php artisan tinker
or a registration system).
Implementing OTP-based login in Laravel was a rewarding experience for me. It’s a secure and modern way to authenticate users without relying solely on passwords. By following these steps, I was able to set up a fully functional OTP system using Laravel’s built-in features and a mail service. You can extend this further by adding SMS-based OTPs or additional security measures. I hope this guide helps you add OTP authentication to your Laravel project easily!
Q: What is OTP-based login?
A: OTP-based login is a secure authentication method where a one-time password is sent to the user’s email or phone, which they use to log in.
Q: Can I use SMS instead of email for OTPs?
A: Yes, you can integrate an SMS service like Twilio or Nexmo to send OTPs via text messages instead of email.
Q: How secure is OTP-based login?
A: OTP-based login is highly secure because the code is temporary and unique. Adding HTTPS and rate-limiting enhances security further.
Q: Can I customize the OTP length?
A: Yes, you can change the OTP length by modifying the rand(100000, 999999)
in the controller to generate a different number of digits.
Q: What if the OTP expires?
A: The system checks the expires_at
timestamp. If the OTP is expired, the user will need to request a new one.
You might also like :