nakanakaii/otp-service

Reusable OTP verification package for Laravel with polymorphic support and event-driven delivery

Maintainers

Package info

github.com/nakanakaii/otp-service

pkg:composer/nakanakaii/otp-service

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-07-28 16:00 UTC

This package is auto-updated.

Last update: 2026-07-28 16:23:00 UTC


README

Latest Version on Packagist License

A reusable Laravel package for OTP verification with polymorphic model support and event-driven delivery. Any model (User, Order, Device, etc.) can use OTP via polymorphic relations. Consumers write their own listener to send OTP via their preferred channel (SMS, WhatsApp, email, etc.).

Requirements

  • PHP 8.1+
  • Laravel 9+

Installation

composer require nakanakaii/otp-service

Publish the config and migration:

php artisan vendor:publish --tag=otp-config,otp-migrations

Run the migration:

php artisan migrate

Setup

1. Write a Listener

The package fires an OtpRequested event when an OTP is generated. Create a listener to send it via your preferred channel:

<?php

namespace App\Listeners;

use App\Models\User;
use Nakanakaii\OtpService\Events\OtpRequested;

class SendOtpNotification
{
    public function handle(OtpRequested $event): void
    {
        /** @var User $user */
        $user = $event->otpable;
        $code = $event->code;

        // Send via your preferred channel
        $user->notify(new OtpNotification($code));
    }
}

2. Register the Listener (Laravel < 11)

In your EventServiceProvider:

use Nakanakaii\OtpService\Events\OtpRequested;
use App\Listeners\SendOtpNotification;

protected $listen = [
    OtpRequested::class => [
        SendOtpNotification::class,
    ],
];

3. Schedule Cleanup

Add to routes/console.php (Laravel 10/11) or Console/Kernel.php (Laravel 10):

// Laravel 11+
use Illuminate\Support\Facades\Schedule;

Schedule::command('otp:clean')->daily();

Or run manually:

php artisan otp:clean

Usage

Generate an OTP

use App\Models\User;
use Nakanakaii\OtpService\Facades\Otp;

$user = User::find(1);
$otp = Otp::generate($user);

// $otp->code — the generated code (e.g. "084721")
// $otp->expires_at — expiration datetime

Verify an OTP

$verified = Otp::verify($user, '084721');

if ($verified) {
    // Code is valid
} else {
    // Invalid or expired code, or max attempts exceeded
}

Check for Pending OTP

if (Otp::hasPending($user)) {
    // Model has an unexpired, unverified OTP
}

Rate Limiting

The package supports opt-in rate limiting using Laravel's cache-backed RateLimiter. When enabled, each model instance is limited to a configurable number of OTP generations within a time window.

Enable in your .env:

OTP_RATE_LIMIT_ENABLED=true
OTP_RATE_LIMIT_MAX=3
OTP_RATE_LIMIT_DECAY=1

When the limit is hit, generate() throws OtpThrottledException:

use Nakanakaii\OtpService\Exceptions\OtpThrottledException;

try {
    $otp = Otp::generate($user);
} catch (OtpThrottledException $e) {
    $seconds = $e->getSecondsUntilAvailable();
    // "Try again in 45 seconds."
}

You can also check the cooldown without triggering the exception:

$seconds = Otp::availableIn($user); // 0 if not throttled

Rate limiting can be scoped by a custom identifier (e.g., IP address) instead of the model:

Otp::generate($user, 'ip:192.168.1.1');
Otp::availableIn($user, 'ip:192.168.1.1');

Configuration

// config/otp.php
return [
    'length'              => env('OTP_LENGTH', 6),
    'expiration_minutes'  => env('OTP_EXPIRATION_MINUTES', 5),
    'max_attempts'        => env('OTP_MAX_ATTEMPTS', 5),

    'rate_limit' => [
        'enabled'       => env('OTP_RATE_LIMIT_ENABLED', false),
        'max_attempts'  => env('OTP_RATE_LIMIT_MAX', 3),
        'decay_minutes' => env('OTP_RATE_LIMIT_DECAY', 1),
    ],
];
Option Default Description
length 6 Number of digits in the OTP code
expiration_minutes 5 Minutes until an OTP expires
max_attempts 5 Verification attempts before invalidation
rate_limit.enabled false Enable/disable OTP request rate limiting
rate_limit.max_attempts 3 Max OTP generations per time window
rate_limit.decay_minutes 1 Time window in minutes

Database

The otp_verifications table structure:

Column Type Description
id bigint Primary key
otpable_type string Polymorphic model class
otpable_id unsignedBigInteger Polymorphic model ID
code string The OTP code
expires_at datetime Expiration timestamp
verified boolean Whether the code was verified
attempts integer Number of verification attempts
created_at timestamp Created timestamp
updated_at timestamp Updated timestamp

Contributing

Please see CONTRIBUTING for details.

Security/Vulnerabilities

If you discover any security-related issues, please create an issue.

Found a bug or have a suggestion?

If you encounter any issues or have any suggestions or find any incorrect or missing data, please feel free to open an issue on GitHub.

Credits

License

The MIT License (MIT). Please see LICENSE for more information.