bbim / otpify
Laravel OTP verification library with multiple drivers
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/bbim/otpify
Requires
- php: ^8.2
- laravel/framework: ^10.0|^11.0|^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0|^11.0
This package is not auto-updated.
Last update: 2025-09-30 17:49:33 UTC
README
A comprehensive Laravel package for OTP (One-Time Password) verification with multiple drivers support, rate limiting, events system, and extensibility features.
โจ Features
- ๐ Multiple Drivers: SMS (Twilio), Email, Absher, Authorization, and custom drivers
- ๐ Secure: Hash-based OTP storage and validation
- โฐ Configurable: Expiration times, code length, and rate limiting
- ๐ฑ Easy Integration: Simple facade and trait-based implementation
- ๐งช Testing Ready: Fake drivers for development and testing
- ๐ Multi-language: Built-in translation support
- ๐ฏ Events System: Hook into OTP lifecycle events
- ๐ก๏ธ Rate Limiting: Prevent abuse and brute force attacks
- ๐ง Extensible: Custom drivers and configurable models
- ๐ Monitoring: Comprehensive event logging
๐ฆ Installation
composer require bbim/otpify
Publish configuration:
php artisan vendor:publish --tag=config
Run migrations:
php artisan migrate
โ๏ธ Configuration
Environment Variables
Add these to your .env
file:
# OTP Configuration OTPIFY_DRIVER=email OTPIFY_CODE_LENGTH=6 OTPIFY_EXPIRATION_MINUTES=3 OTPIFY_RATE_LIMITING_ENABLED=true OTPIFY_MAX_SENDS=3 OTPIFY_RATE_LIMIT_DECAY=10 OTPIFY_MAX_VERIFY_ATTEMPTS=5 OTPIFY_LOCKOUT_MINUTES=30 # Twilio Configuration (for SMS driver) OTPIFY_TWILIO_SID=your_twilio_sid OTPIFY_TWILIO_TOKEN=your_twilio_token OTPIFY_TWILIO_FROM=+1234567890 # Absher Configuration ABSHER_API_KEY=your_absher_api_key ABSHER_HOST=https://api.absher.sa # Events Configuration OTPIFY_EVENTS_ENABLED=true OTPIFY_EVENTS_PRESET=all_enabled OTPIFY_EVENT_OTP_SENDING=true OTPIFY_EVENT_OTP_SENT=true OTPIFY_EVENT_OTP_VERIFYING=true OTPIFY_EVENT_OTP_VERIFIED=true OTPIFY_EVENT_OTP_VERIFICATION_FAILED=true
Model Setup
Implement the Otpifiable
contract in your User model:
use Bbim\Otpify\Contracts\Otpifiable; use Bbim\Otpify\Traits\CanOtpifyCode; class User extends Authenticatable implements Otpifiable { use CanOtpifyCode; public function doesRequireVerifyingByOtp(Request $request): bool { // Your logic to determine if OTP is required return !$this->hasVerifiedEmail(); } public function getPhoneNumber(): string { return $this->phone; } public function getNationalId(): string { return $this->national_id; } public function getMorphClass() { return static::class; } public function getKey() { return $this->getKey(); } }
๐ Usage
Basic Usage
use Bbim\Otpify\Facades\Otpify; // Send OTP $otpCode = Otpify::driver('email')->send($request, $user); // Verify OTP $isValid = Otpify::driver('email')->verify($request, $otpCode->id, '123456');
Using Different Drivers
// Email driver $otpCode = Otpify::driver('email')->send($request, $user); // SMS driver (Twilio) $otpCode = Otpify::driver('twilio')->send($request, $user); // Absher driver $otpCode = Otpify::driver('absher')->send($request, $user); // Fake driver (for testing) $otpCode = Otpify::driver('fake_absher')->send($request, $user);
With Additional Verification
$isValid = Otpify::driver('email')->verify($request, $otpCode->id, '123456', function($request, $otpifyCode) { // Additional verification logic return $request->ip() === $otpifyCode->data['ip_address']; });
๐ฏ Custom Drivers
Create your own OTP driver:
Step 1: Create Driver Class
namespace App\Otpify\Drivers; use Bbim\Otpify\Contracts\OtpifyDriverInterface; use Bbim\Otpify\Contracts\Otpifiable; use Bbim\Otpify\Models\OtpifyCode; use Bbim\Otpify\Traits\CanOtpifyCode; use Bbim\Otpify\Traits\RateLimitsOtp; use Illuminate\Http\Request; class WhatsAppDriver implements OtpifyDriverInterface { use CanOtpifyCode, RateLimitsOtp; public function send(Request $request, Otpifiable $otpifiable, array $data = []): OtpifyCode { // Check rate limiting if ($this->hasTooManySendAttempts($otpifiable)) { throw new \Bbim\Otpify\Exceptions\TooManyOtpAttemptsException( $this->availableInForSend($otpifiable) ); } $code = generateRandomCode(config('otpify.code_length')); // Your WhatsApp implementation $this->sendWhatsAppMessage($otpifiable->getPhoneNumber(), $code); $otpifyCode = $this->createOtpifyCode($code, $otpifiable, auth()->user(), $data); // Increment attempts $this->incrementSendAttempts($otpifiable); return $otpifyCode; } public function doesRequireVerifyingByOtp(Request $request, Otpifiable $otpifiable): bool { return $otpifiable->doesRequireVerifyingByOtp($request); } public function verify(Request $request, $vid, $code, ?\Closure $additionalCheckCallback = null): string|bool { // Check rate limiting if ($this->hasTooManyVerifyAttempts($vid)) { throw new \Bbim\Otpify\Exceptions\TooManyOtpAttemptsException( $this->availableInForVerify($vid) ); } try { $otpifyCode = $this->getOtpifyCode($vid); $this->verifyOtpifyCode($otpifyCode, $request, $code, $additionalCheckCallback); $this->setOtpExpiredAt($otpifyCode); // Clear rate limiting on success $this->clearVerifyAttempts($vid); return true; } catch (\Throwable $e) { // Increment failed attempts $this->incrementVerifyAttempts($vid); throw $e; } } private function sendWhatsAppMessage(string $phone, string $code): void { // Your WhatsApp API implementation } }
Step 2: Register in Config
// config/otpify.php 'custom_drivers' => [ 'whatsapp' => \App\Otpify\Drivers\WhatsAppDriver::class, ],
Step 3: Use It
use Bbim\Otpify\Facades\Otpify; $otpCode = Otpify::driver('whatsapp')->send($request, $user);
๐ Events
Event Configuration
Global Control
# Master switch for all events OTPIFY_EVENTS_ENABLED=true # Set preset (all_enabled, all_disabled, critical_only, debug_mode) OTPIFY_EVENTS_PRESET=all_enabled
Individual Event Control
# Control each event separately OTPIFY_EVENT_OTP_SENDING=true OTPIFY_EVENT_OTP_SENT=true OTPIFY_EVENT_OTP_VERIFYING=true OTPIFY_EVENT_OTP_VERIFIED=true OTPIFY_EVENT_OTP_VERIFICATION_FAILED=true
Configuration File
// config/otpify.php 'events' => [ 'enabled' => true, // Master switch 'preset' => 'all_enabled', // Current preset 'individual' => [ // Override specific events 'otp_sending' => true, 'otp_sent' => true, // ... ], 'presets' => [ // Available presets 'all_enabled' => [...], 'all_disabled' => [...], 'critical_only' => [...], 'debug_mode' => [...], ], ],
Available Presets
Preset | Description | Events Enabled |
---|---|---|
all_enabled |
All events enabled (default) | All |
all_disabled |
All events disabled | None |
critical_only |
Only critical events | otp_sent , otp_verified , otp_verification_failed |
debug_mode |
All events for debugging | All |
Event Listeners
Listen to OTP lifecycle events:
// app/Providers/EventServiceProvider.php protected $listen = [ \Bbim\Otpify\Events\OtpSending::class => [ \App\Listeners\LogOtpSending::class, ], \Bbim\Otpify\Events\OtpSent::class => [ \App\Listeners\LogOtpSent::class, ], \Bbim\Otpify\Events\OtpVerified::class => [ \App\Listeners\HandleOtpVerified::class, ], \Bbim\Otpify\Events\OtpVerificationFailed::class => [ \App\Listeners\LogOtpFailure::class, ], ];
Example listener:
namespace App\Listeners; use Bbim\Otpify\Events\OtpSent; class LogOtpSent { public function handle(OtpSent $event): void { logger()->info('OTP sent', [ 'user' => $event->otpifiable->getKey(), 'otp_id' => $event->otpifyCode->id, 'driver' => $event->otpifyCode->driver, ]); } }
Command Line Control
Control events from the command line:
# Show current events status php artisan otpify:events status # Enable all events php artisan otpify:events enable --all # Disable all events php artisan otpify:events disable --all # Enable specific event php artisan otpify:events enable --event otp_sent # Disable specific event php artisan otpify:events disable --event otp_sending # Set preset php artisan otpify:events preset --preset critical_only # Reset to defaults php artisan otpify:events reset
Programmatic Control
Control events programmatically:
use Bbim\Otpify\Helpers\EventController; // Enable all events EventController::enableAll(); // Disable all events EventController::disableAll(); // Enable specific event EventController::enableEvent('otp_sent'); // Disable specific event EventController::disableEvent('otp_sending'); // Set preset EventController::setPreset('critical_only'); // Check if event is enabled if (EventController::isEventEnabled('otp_verified')) { // Event is enabled } // Get all events status $status = EventController::getAllEventsStatus(); // Get full configuration $config = EventController::getConfiguration(); // Apply custom configuration EventController::applyConfiguration([ 'enabled' => true, 'preset' => 'critical_only', 'individual' => [ 'otp_sending' => false, 'otp_verified' => true, ] ]);
Performance Optimization
Disable events you don't need to improve performance:
# Disable all events OTPIFY_EVENTS_ENABLED=false # Or use critical_only preset OTPIFY_EVENTS_PRESET=critical_only # Or disable specific events OTPIFY_EVENT_OTP_SENDING=false OTPIFY_EVENT_OTP_VERIFYING=false
๐ก๏ธ Rate Limiting
Configure rate limiting in .env
:
OTPIFY_RATE_LIMITING_ENABLED=true OTPIFY_MAX_SENDS=3 OTPIFY_RATE_LIMIT_DECAY=10 OTPIFY_MAX_VERIFY_ATTEMPTS=5 OTPIFY_LOCKOUT_MINUTES=30
Disable for testing:
OTPIFY_RATE_LIMITING_ENABLED=false
๐ง Custom Models
Extend OTP models:
namespace App\Models; use Bbim\Otpify\Models\OtpifyCode as BaseOtpifyCode; class OtpifyCode extends BaseOtpifyCode { // Add your custom methods public function user() { return $this->belongsTo(User::class, 'otpifiable_id'); } public function scopeRecent($query) { return $query->where('created_at', '>=', now()->subMinutes(10)); } }
Register in config:
// config/otpify.php 'models' => [ 'otp_code' => \App\Models\OtpifyCode::class, ],
๐งช Testing
Use fake drivers for testing:
// In your test Otpify::shouldReceive('driver') ->with('fake_absher') ->andReturn(new \Bbim\Otpify\Drivers\FakeAbsherDriver()); $otpCode = Otpify::driver('fake_absher')->send($request, $user);
๐ Available Drivers
Driver | Description | Configuration |
---|---|---|
email |
Send OTP via email | MAIL_* settings |
twilio |
Send OTP via SMS | OTPIFY_TWILIO_* settings |
absher |
Absher integration | ABSHER_* settings |
fake_absher |
Fake Absher for testing | Test configuration |
authorization |
Custom authorization | Custom implementation |
fake_authorization |
Fake authorization for testing | Test configuration |
๐ง Configuration Options
Rate Limiting
'rate_limiting' => [ 'enabled' => true, 'max_sends' => 3, // Max OTP sends per user 'decay_minutes' => 10, // Cooldown period 'max_verify_attempts' => 5, // Max verification attempts 'lockout_minutes' => 30, // Lockout duration ],
Verification Methods
'verification_methods' => [ 'twilio' => 'phone', 'email' => 'email', 'absher' => 'email', // ... other drivers ],
๐จ Error Handling
The package provides specific exceptions for different scenarios:
OtpCodeNotFoundException
: OTP code not foundOtpCodeExpiredException
: OTP code has expiredOtpCodeIncorrectException
: Incorrect OTP codeOtpCodeAlreadyUsedException
: OTP code already usedTooManyOtpAttemptsException
: Rate limit exceededOtpifiableNotEqualAuthUserException
: User mismatch
๐ Requirements
- PHP >= 8.2
- Laravel >= 10.0
- Propaganistas/LaravelPhone (for phone number validation)
๐ค Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
๐ License
This package is open-sourced software licensed under the MIT license.
๐ Support
If you encounter any issues or have questions, please:
- Check the Issues page
- Create a new issue with detailed information
- Contact the maintainers
Made with โค๏ธ by the BBIM team