elkomy / laravel-otp
Secure, queued, and configurable OTP verification package for Laravel applications.
Requires
Requires (Dev)
- laravel/pint: ^1.14
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.0|^11.0|^12.0
README
A production-ready, secure, and queued One-Time Password (OTP) verification package for Laravel applications. Designed to handle email verification during account registration, password reset validation, login two-factor verification, and any custom verification workflow.
🇸🇦 اضغط هنا لقراءة الدليل الكامل باللغة العربية (Arabic Documentation)
📚 Detailed Documentation
- 📦 Step-by-Step Installation & Setup
- ✉️ Email Validation & Security Handling (Unregistered vs Registered Emails)
- 📝 Account Registration & Email Verification Flow
- 🔑 Password Reset Flow with OTP
- 🇸🇦 الدليل الشامل باللغة العربية
🌟 Key Features
- ⚡ Queued Email Delivery: Emails are dispatched via Laravel Queues (Redis, Database, SQS, etc.) without blocking user requests.
- 🔒 Cryptographically Secure Generation: Uses
random_int()to generate secure numeric OTP codes. - 🛡️ Secure OTP Hashing: Plaintext codes are never stored in the database; hashes are securely stored and verified.
- ⏳ Expiration Handling: Automatic expiration enforcement (configurable, default 10 minutes).
- 🚫 Brute-Force & Attempt Limits: Configurable maximum failed attempts limit before invalidating the OTP (default 5 attempts).
- 🛑 Resend Throttling: Rate limits repeated OTP generation requests for the same recipient (default 60 seconds delay).
- 🔄 Replay & Invalidation Protection: Single-use verification; previous active tokens are automatically invalidated.
- 🎯 Generic Purpose Architecture: Supports multiple workflows (
email_verification,password_reset,login, or any custom purpose). - ✨ Form Request Validation Rule: Built-in
OtpRuleto validate OTP codes directly in Laravel FormRequests. - 📦 Infrastructure Independent: Works seamlessly with any database, queue connection, and mail provider supported by Laravel.
- 📢 Events & Exceptions: Rich domain events and specific exceptions for clean error handling in controllers.
📋 Requirements
- PHP 8.2 or higher
- Laravel 10.x, 11.x, 12.x, or 13.x
🚀 Quick Step-by-Step Installation
Step 1: Install via Composer
composer require elkomy/laravel-otp
Step 2: Publish Config and Migrations
php artisan vendor:publish --provider="Elkomy\LaravelOtp\LaravelOtpServiceProvider"
Step 3: Run Database Migrations
php artisan migrate
Step 4: Run Queue Worker
php artisan queue:work --queue=default
⚙️ Configuration
The published config/otp.php file allows you to customize every aspect of OTP behavior:
return [ // Table name in your database 'table_name' => 'otps', // Number of digits (between 4 and 10, default: 6) 'digits' => 6, // Expiration duration in minutes (default: 10) 'expires_in_minutes' => 10, // Maximum failed attempts before token is locked (default: 5) 'max_attempts' => 5, // Throttling delay between resend requests in seconds (default: 60) 'resend_throttle_seconds' => 60, // Queue configuration for sending notification emails 'queue' => [ 'enabled' => true, 'connection' => env('OTP_QUEUE_CONNECTION', null), 'queue' => env('OTP_QUEUE_NAME', 'default'), ], // Email notification settings 'mail' => [ 'subject' => 'Your Verification Code', 'greeting' => 'Hello!', 'line' => 'Your one-time verification code is:', 'footer' => 'This code will expire in :minutes minutes. If you did not request this, please ignore this email.', 'view' => null, // Optional custom Blade view (e.g. 'emails.otp') ], ];
📖 Usage Examples
1. Account Registration with Email Verification
use App\Models\User; use Elkomy\LaravelOtp\Facades\Otp; use Illuminate\Http\Request; // In your Registration Controller: public function register(Request $request) { $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], 'password' => ['required', 'string', 'min:8'], ]); // 1. Create the user $user = User::create([ 'name' => $validated['name'], 'email' => strtolower(trim($validated['email'])), 'password' => bcrypt($validated['password']), ]); // 2. Send the OTP verification code via queued email Otp::send( identifier: $user->email, purpose: 'email_verification', metadata: ['user_id' => $user->id] ); return response()->json([ 'message' => 'Registration successful. A verification code has been sent to your email.', 'email' => $user->email, ], 201); }
2. Validating OTP Code in Form Requests (OtpRule)
You can validate OTP codes effortlessly using OtpRule:
namespace App\Http\Requests; use Elkomy\LaravelOtp\Rules\OtpRule; use Illuminate\Foundation\Http\FormRequest; class VerifyOtpRequest extends FormRequest { public function rules(): array { return [ 'email' => ['required', 'email'], 'code' => [ 'required', 'string', 'size:6', new OtpRule( identifier: (string) $this->input('email'), purpose: 'email_verification', consume: true // Automatically marks verified if valid ), ], ]; } }
3. Handling Unregistered Emails in Password Reset
To prevent User Enumeration Attacks, always return a generic message:
use App\Models\User; use Elkomy\LaravelOtp\Facades\Otp; public function sendResetOtp(Request $request) { $request->validate(['email' => ['required', 'email']]); $email = strtolower(trim($request->email)); // Check if registered $user = User::where('email', $email)->first(); if ($user !== null) { Otp::send(identifier: $user->email, purpose: 'password_reset'); } // Always generic response return response()->json([ 'message' => 'If an account exists with this email, a verification code has been sent.', ]); }
🛠️ API Reference
Otp Facade Methods
| Method | Description |
|---|---|
Otp::send($identifier, $purpose = 'email_verification', $metadata = null) |
Generates, hashes, stores, and queues OTP notification. |
Otp::sendPasswordReset($email, $resetUrl = null, $metadata = null) |
Generates, hashes, and queues password reset OTP with direct reset link. |
Otp::verify($identifier, $code, $purpose = 'email_verification') |
Verifies code and marks OTP as verified. |
Otp::verifyPasswordReset($email, $code) |
Verifies password reset code and marks it as verified. |
Otp::check($identifier, $code, $purpose = 'email_verification') |
Validates code without consuming it (read-only). |
Otp::createResetUrl($baseUrl, $email, $code) |
Builds a full reset URL with query parameters. |
Otp::resend($identifier, $purpose = 'email_verification', $metadata = null) |
Resends a fresh OTP while enforcing throttle limits. |
Otp::invalidate($identifier, $purpose = null) |
Expires all active OTP tokens for the identifier. |
🛑 Exceptions Reference
All package exceptions extend Elkomy\LaravelOtp\Exceptions\OtpException:
OtpNotFoundException: No active OTP found for the given identifier and purpose.OtpInvalidException: The provided code does not match.OtpExpiredException: The code has expired.OtpTooManyAttemptsException: Maximum failed verification attempts exceeded.OtpAlreadyVerifiedException: The code was already used/verified.OtpResendTooSoonException: Request throttled (access$e->secondsRemaining).
🐳 Docker Development & Testing
Run all package commands inside Docker without requiring PHP or Composer on your host:
# Build the container docker compose build # Install dependencies docker compose run --rm php-cli composer install # Run the test suite docker compose run --rm php-cli vendor/bin/phpunit # Format code with Laravel Pint docker compose run --rm php-cli vendor/bin/pint # Run static analysis with PHPStan docker compose run --rm php-cli vendor/bin/phpstan analyse
📄 License
The MIT License (MIT). Please see LICENSE for more information.