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

dev-main 2025-09-30 13:21 UTC

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 found
  • OtpCodeExpiredException: OTP code has expired
  • OtpCodeIncorrectException: Incorrect OTP code
  • OtpCodeAlreadyUsedException: OTP code already used
  • TooManyOtpAttemptsException: Rate limit exceeded
  • OtpifiableNotEqualAuthUserException: User mismatch

๐Ÿ“‹ Requirements

  • PHP >= 8.2
  • Laravel >= 10.0
  • Propaganistas/LaravelPhone (for phone number validation)

๐Ÿค Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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:

  1. Check the Issues page
  2. Create a new issue with detailed information
  3. Contact the maintainers

Made with โค๏ธ by the BBIM team