PHPackages                             biponix/laravel-secure-otp - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. biponix/laravel-secure-otp

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

biponix/laravel-secure-otp
==========================

Laravel OTP generator and validator

v1.1.0(6mo ago)18280[3 PRs](https://github.com/biponix/laravel-secure-otp/pulls)MITPHPPHP ^8.1|^8.2|^8.3|^8.4CI passing

Since Oct 30Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/biponix/laravel-secure-otp)[ Packagist](https://packagist.org/packages/biponix/laravel-secure-otp)[ Docs](https://github.com/biponix/laravel-secure-otp)[ GitHub Sponsors](https://github.com/biponix)[ RSS](/packages/biponix-laravel-secure-otp/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (13)Versions (6)Used By (0)

Laravel Secure OTP
==================

[](#laravel-secure-otp)

[![Latest Version on Packagist](https://camo.githubusercontent.com/dcd4c732e39e6c56652bde3ec6a197550531d8e1c34bf0ce3a30f0fb578388d1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6269706f6e69782f6c61726176656c2d7365637572652d6f74702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/biponix/laravel-secure-otp)[![GitHub Tests Action Status](https://camo.githubusercontent.com/4447a73d0bd21c51afa4c95defa1f37c74f3d5d24be0043f94834e0c6892f4b9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6269706f6e69782f6c61726176656c2d7365637572652d6f74702f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/biponix/laravel-secure-otp/actions/workflows/run-tests.yml)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/18e9c662b53be1526c768ffa72da4ed77dfd574fd953a4a14599012b1c363abd/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6269706f6e69782f6c61726176656c2d7365637572652d6f74702f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/biponix/laravel-secure-otp/actions/workflows/fix-php-code-style-issues.yml)[![Total Downloads](https://camo.githubusercontent.com/0911f92c462fef36cc142e5f7e743b5f2c4d9b23e5fce7ac2fc1927fa415fa71/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6269706f6e69782f6c61726176656c2d7365637572652d6f74703f7374796c653d666c61742d737175617265)](https://packagist.org/packages/biponix/laravel-secure-otp)

A production-ready, secure OTP (One-Time Password) package for Laravel applications. Generate and verify OTP codes via Email, SMS, WhatsApp, or any Laravel notification channel.

Features
--------

[](#features)

- ✅ **Production-Grade Security**: HMAC-based storage with secret key, timing-attack resistant verification
- ✅ **Multi-Channel Support**: Email, SMS, WhatsApp, Telegram (via Laravel Notifications)
- ✅ **Pluggable Identifier Types**: Extensible validation/normalization for emails, phones, usernames, user IDs, etc.
- ✅ **Context-Safe**: Works seamlessly in HTTP, queue workers, and console commands
- ✅ **Context-Aware Rate Limiting**: Separate limits for generation vs verification (brute force protection)
- ✅ **Multi-Layer Protection**: Per-identifier + per-IP rate limiting in HTTP contexts
- ✅ **Attack Prevention**: Replay attack prevention, race condition protection with distributed cache locks
- ✅ **Fully Customizable**: Custom notification classes, configurable expiry, length, attempts
- ✅ **Security Logging**: Detailed audit logs with privacy-preserving PII masking
- ✅ **100% Test Coverage**: 104 comprehensive tests ensuring reliability
- ✅ **Wide Compatibility**: PHP 8.1-8.4, Laravel 10-12

Installation
------------

[](#installation)

You can install the package via composer:

```
composer require biponix/laravel-secure-otp
```

Run the migrations:

```
php artisan migrate
```

The migrations will run automatically from the package. If you need to customize the migration, you can publish it first:

```
php artisan vendor:publish --tag="secure-otp-migrations"
php artisan migrate
```

Publish the config file (optional):

```
php artisan vendor:publish --tag="secure-otp-config"
```

Configuration
-------------

[](#configuration)

The config file (`config/secure-otp.php`) allows you to customize:

```
return [
    // OTP expiry in minutes (default: 5)
    'expiry_minutes' => env('OTP_EXPIRY_MINUTES', 5),

    // OTP code length (default: 6 digits)
    'length' => env('OTP_LENGTH', 6),

    // Maximum verification attempts (default: 3)
    'max_attempts' => env('OTP_MAX_ATTEMPTS', 3),

    // Hash algorithm and secret for HMAC (prevents rainbow table attacks)
    'hash_algorithm' => env('OTP_HASH_ALGORITHM', 'sha256'),
    'hash_secret' => env('OTP_HASH_SECRET', null), // Falls back to app.key if null

    // Context-aware rate limiting (separate limits for generation vs verification)
    'rate_limits' => [
        // Cache key prefix (prevents collisions in shared cache)
        'prefix' => env('OTP_RATE_LIMIT_PREFIX', 'secure-otp'),

        // Shared defaults (used for both generate and verify if context-specific not set)
        'per_identifier' => [
            'max_attempts' => env('OTP_RATE_LIMIT_IDENTIFIER', 3),
            'decay_seconds' => env('OTP_RATE_LIMIT_IDENTIFIER_DECAY', 3600), // 1 hour
        ],
        'per_ip' => [
            'max_attempts' => env('OTP_RATE_LIMIT_IP', 10),
            'decay_seconds' => env('OTP_RATE_LIMIT_IP_DECAY', 3600), // 1 hour
        ],

        // Optional: Override limits specifically for verification (prevent brute force)
        'verify_per_identifier' => [
            'max_attempts' => env('OTP_VERIFY_RATE_LIMIT_IDENTIFIER', 5),
            'decay_seconds' => env('OTP_VERIFY_RATE_LIMIT_IDENTIFIER_DECAY', 60), // 1 minute
        ],
        'verify_per_ip' => [
            'max_attempts' => env('OTP_VERIFY_RATE_LIMIT_IP', 20),
            'decay_seconds' => env('OTP_VERIFY_RATE_LIMIT_IP_DECAY', 60), // 1 minute
        ],
    ],

    // Custom notification class
    'notification_class' => env('OTP_NOTIFICATION_CLASS', \Biponix\SecureOtp\Notifications\OtpNotification::class),

    // Cleanup after hours (default: 24)
    'cleanup_after_hours' => env('OTP_CLEANUP_AFTER_HOURS', 24),

    // Enable security logging (default: true)
    'enable_logging' => env('OTP_ENABLE_LOGGING', true),
];
```

Usage
-----

[](#usage)

### Quick Start

[](#quick-start)

**Without Type Validation (Pass-through Mode)**

```
use Biponix\SecureOtp\Services\SecureOtpService;

$otp = app(SecureOtpService::class);

// Send OTP to any identifier (no validation)
$otp->send('01700000000');        // Bangladesh phone
$otp->send('user@example.com');   // Email
$otp->send('username123');        // Username
$otp->send('12345');              // User ID

// Verify OTP
$verified = $otp->verify('01700000000', '123456');
```

**With Type Validation** (Recommended for production)

```
use Biponix\SecureOtp\Services\SecureOtpService;
use Biponix\SecureOtp\Types\EmailType;

// Register identifier types in AppServiceProvider::boot()
SecureOtpService::addType('email', new EmailType());

// Now use with type parameter
$otp->send('user@example.com', 'email');     // ✅ Validated & normalized
$otp->verify('user@example.com', '123456', 'email');
```

### Basic Usage Example

[](#basic-usage-example)

```
use Biponix\SecureOtp\Exceptions\InvalidIdentifierException;
use Biponix\SecureOtp\Exceptions\RateLimitExceededException;
use Biponix\SecureOtp\Exceptions\OtpGenerationException;
use Biponix\SecureOtp\Services\SecureOtpService;

class AuthController extends Controller
{
    public function sendOtp(Request $request, SecureOtpService $otp)
    {
        try {
            // Send OTP (throws on error)
            $otp->send($request->email, 'email');

            return response()->json([
                'message' => 'OTP sent successfully'
            ]);

        } catch (RateLimitExceededException $e) {
            // Rate limit exceeded
            return response()->json([
                'error' => 'Too many requests',
                'retry_after' => $e->getRetryAfter(),
            ], 429);

        } catch (InvalidIdentifierException $e) {
            return response()->json(['error' => 'Invalid email address'], 400);

        } catch (OtpGenerationException $e) {
            return response()->json(['error' => 'Failed to send OTP'], 500);
        }
    }

    public function verifyOtp(Request $request, SecureOtpService $otp)
    {
        // verify() returns bool (doesn't expose why it failed for security)
        $verified = $otp->verify($request->email, $request->code, 'email');

        if ($verified) {
            // OTP verified successfully
            $user = User::where('email', $request->email)->firstOrFail();
            auth()->login($user);

            return response()->json(['message' => 'Login successful']);
        }

        return response()->json(['error' => 'Invalid or expired code'], 422);
    }
}
```

### Using with Dependency Injection

[](#using-with-dependency-injection)

```
use Biponix\SecureOtp\Services\SecureOtpService;

public function __construct(
    private SecureOtpService $otp
) {}

public function sendCode(string $identifier): void
{
    // Throws exceptions on error
    $this->otp->send($identifier);
}
```

### Custom Identifier Types

[](#custom-identifier-types)

Create custom identifier types for phones, usernames, or any identifier format you need.

**Step 1: Create Type Class**

```
// app/Otp/BangladeshSmsType.php
namespace App\Otp;

use Biponix\SecureOtp\Contracts\OtpIdentifierType;

class BangladeshSmsType extends OtpIdentifierType
{
    /**
     * Normalize Bangladesh phone numbers to E.164 format
     */
    public function normalize(string $value): string
    {
        // Remove spaces, dashes, parentheses
        $value = preg_replace('/[\s\-\(\)]/', '', $value);

        // Convert local format (01700000000) to E.164 (+8801700000000)
        if (preg_match('/^0\d{10}$/', $value)) {
            return '+880' . substr($value, 1);
        }

        return $value;
    }

    /**
     * Validate E.164 Bangladesh phone numbers
     */
    public function validate(string $value): bool
    {
        // Must be +880 followed by 10 digits
        return preg_match('/^\+880\d{10}$/', $value) === 1;
    }
}
```

**Step 2: Register Type in AppServiceProvider**

```
// app/Providers/AppServiceProvider.php
use App\Otp\BangladeshSmsType;
use Biponix\SecureOtp\Services\SecureOtpService;

public function boot(): void
{
    // Register custom identifier types
    SecureOtpService::addType('sms', new BangladeshSmsType());
}
```

**Step 3: Use With Type Parameter**

```
// Send OTP with validation
$otp->send('01700000000', 'sms');      // ✅ Normalized to +8801700000000
$otp->send('0170-000-0000', 'sms');    // ✅ Normalized to +8801700000000

// Verify with same type
$verified = $otp->verify('01700000000', '123456', 'sms');  // ✅ Works!
```

**More Examples:**

```
// Username type
class UsernameType extends OtpIdentifierType
{
    public function normalize(string $value): string
    {
        return strtolower(trim($value));
    }

    public function validate(string $value): bool
    {
        return preg_match('/^[a-z0-9_]{3,20}$/', $value) === 1;
    }
}

// User ID type
class UserIdType extends OtpIdentifierType
{
    public function normalize(string $value): string
    {
        return trim($value);
    }

    public function validate(string $value): bool
    {
        return ctype_digit($value) && (int)$value > 0;
    }
}

// Register in AppServiceProvider
SecureOtpService::addType('username', new UsernameType());
SecureOtpService::addType('user_id', new UserIdType());

// Usage
$otp->send('john_doe', 'username');
$otp->send('12345', 'user_id');
```

### Generate Without Sending (Custom Delivery)

[](#generate-without-sending-custom-delivery)

```
use Biponix\SecureOtp\Services\SecureOtpService;
use Biponix\SecureOtp\Exceptions\RateLimitExceededException;

public function customDelivery(SecureOtpService $otp)
{
    try {
        // Generate OTP without sending (returns string)
        $code = $otp->generate('user@example.com', 'email');

        // Deliver via your custom method
        $this->sendViaSms($code);

    } catch (RateLimitExceededException $e) {
        // Handle rate limiting
        return response()->json([
            'error' => 'Too many requests',
            'retry_after' => $e->getRetryAfter(),
        ], 429);
    }
}
```

### Send Synchronously (Block Until Sent)

[](#send-synchronously-block-until-sent)

```
// Default: queued (non-blocking)
$otp->send('user@example.com');

// Force synchronous sending (blocks until sent)
$otp->sendNow('user@example.com');
```

### Using the Facade (Optional)

[](#using-the-facade-optional)

```
use Biponix\SecureOtp\Facades\SecureOtp;

// Send OTP (throws exceptions on error)
SecureOtp::send('user@example.com');

// Verify OTP (returns bool)
$verified = SecureOtp::verify('user@example.com', '123456');

// Generate without sending (returns string, throws on rate limit)
$code = SecureOtp::generate('user@example.com');

// Send synchronously (throws exceptions on error)
SecureOtp::sendNow('user@example.com');
```

### Custom Notification Channels

[](#custom-notification-channels)

Create your own notification class to route OTPs via SMS, WhatsApp, or other channels based on identifier type.

**Type-Based Channel Routing:**

```
// app/Notifications/MultiChannelOtpNotification.php
namespace App\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\VonageSmsMessage;
use Illuminate\Notifications\Notification;

class MultiChannelOtpNotification extends Notification
{
    public function __construct(public string $code) {}

    /**
     * Route notification channels based on identifier type
     */
    public function via(object $notifiable): array
    {
        // $notifiable->type comes from SecureOtpService::send($identifier, $type)
        return match ($notifiable->type) {
            'sms' => ['vonage'],           // Phone via SMS
            'email' => ['mail'],           // Email
            'whatsapp' => ['whatsapp'],    // WhatsApp (if configured)
            default => ['mail'],           // Fallback to email
        };
    }

    /**
     * SMS notification
     */
    public function toVonage(object $notifiable): VonageSmsMessage
    {
        return (new VonageSmsMessage)
            ->content("Your verification code is: {$this->code}");
    }

    /**
     * Email notification
     */
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Your Verification Code')
            ->line("Your verification code is: {$this->code}")
            ->line('This code will expire in ' . config('secure-otp.expiry_minutes', 5) . ' minutes.');
    }
}
```

**Register Your Notification:**

Update your `.env`:

```
OTP_NOTIFICATION_CLASS="App\Notifications\MultiChannelOtpNotification"
```

**Usage:**

```
// Sends via Vonage SMS
$otp->send('01700000000', 'sms');

// Sends via Email
$otp->send('user@example.com', 'email');

// Sends via WhatsApp (if configured)
$otp->send('+8801700000000', 'whatsapp');
```

### Cleanup Expired OTPs

[](#cleanup-expired-otps)

The package requires scheduled cleanup to remove expired OTP records from the database.

**Step 1: Add to Laravel Scheduler**

In Laravel 11+, add to `routes/console.php`:

```
use Illuminate\Support\Facades\Schedule;

Schedule::command('secure-otp:clean --force')
    ->daily()
    ->withoutOverlapping()
    ->onOneServer();
```

Or in `app/Console/Kernel.php` (Laravel 10 and below):

```
protected function schedule(Schedule $schedule)
{
    $schedule->command('secure-otp:clean --force')
             ->daily()
             ->withoutOverlapping()
             ->onOneServer();
}
```

**Step 2: Ensure Cron is Running**

Make sure your server has the Laravel scheduler cron job configured:

```
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
```

**Manual Cleanup (Optional)**

```
# In development (prompts for confirmation)
php artisan secure-otp:clean

# In production (bypasses confirmation)
php artisan secure-otp:clean --force
```

**Programmatic Cleanup (Advanced)**

```
use Biponix\SecureOtp\Services\SecureOtpService;

$deleted = app(SecureOtpService::class)->cleanupExpired();
// Returns the number of deleted records
```

Security Features
-----------------

[](#security-features)

### 1. HMAC-Based Storage (Rainbow Table Protection)

[](#1-hmac-based-storage-rainbow-table-protection)

OTP codes are hashed using **HMAC-SHA256 with a secret key** before storage. This prevents rainbow table attacks even if the database is compromised. Plain codes are never saved.

```
// Configure in .env
OTP_HASH_SECRET=your-secret-key  // Falls back to APP_KEY if not set
```

### 2. Timing-Safe Comparison

[](#2-timing-safe-comparison)

Uses `hash_equals()` to prevent timing attacks during verification.

### 3. Context-Aware Rate Limiting (Brute Force Protection)

[](#3-context-aware-rate-limiting-brute-force-protection)

**Separate rate limits for generation vs verification** to balance security and user experience:

#### Generation (Sending OTP)

[](#generation-sending-otp)

- **Per Identifier**: 3 attempts/hour (prevents spam to a user)
- **Per IP**: 10 attempts/hour (prevents mass spamming from one IP)

#### Verification (Checking OTP)

[](#verification-checking-otp)

- **Per Identifier**: 5 attempts/minute (more lenient, user may typo)
- **Per IP**: 20 attempts/minute (prevents distributed brute force across multiple accounts)

**Key Features:**

- **Smart Detection**: IP rate limiting automatically skipped in queue/console contexts
- **Flexible Configuration**: Supports context-specific overrides (`verify_per_identifier`) or falls back to shared config
- **Cache Key Isolation**: Uses context-aware keys (e.g., `secure-otp:verify:identifier:user@example.com`)
- **Per-Axis Control**: Each rate limiting axis can be disabled independently by setting to `null` or `false`

### 4. Generic Responses

[](#4-generic-responses)

Returns boolean values instead of detailed error messages to prevent enumeration attacks.

### 5. Race Condition Protection

[](#5-race-condition-protection)

Uses distributed cache locks (`Cache::lock()`) combined with database transactions and row-level locks (`lockForUpdate()`) to serialize OTP generation and ensure only one valid OTP exists per identifier at any time. Lock timeouts (3 seconds) provide friendly error messages under high concurrency.

### 6. Replay Attack Prevention

[](#6-replay-attack-prevention)

Previous OTPs are automatically invalidated when a new one is generated.

### 7. Attempt Limiting

[](#7-attempt-limiting)

Maximum verification attempts per OTP (default: 3) to prevent brute force attacks.

### 8. Security Logging with Privacy

[](#8-security-logging-with-privacy)

Logs all security events (invalid codes, rate limits, etc.) with PII masking:

- Emails: `te***@example.com`
- Phones: `***7890`

API Reference
-------------

[](#api-reference)

### `generate(string $identifier, ?string $type = null): string`

[](#generatestring-identifier-string-type--null-string)

Generates an OTP code without sending it (for custom delivery methods).

**Parameters:**

- `$identifier` (string): Email, phone, username, or any identifier
- `$type` (string|null): Optional. Identifier type for validation/normalization (e.g., 'email', 'sms', 'username')

**Returns:**

- `string`: The generated OTP code

**Throws:**

- `RateLimitExceededException`: If rate limit is exceeded
- `InvalidIdentifierException`: If security check fails or type validation fails
- `OtpGenerationException`: If OTP generation fails

**Examples:**

```
try {
    $code = $otp->generate('user@example.com', 'email');  // With validation
    $code = $otp->generate('01700000000');                // Without validation
} catch (RateLimitExceededException $e) {
    // Handle rate limiting: $e->getRetryAfter() gives seconds until retry
}
```

---

### `send(string $identifier, ?string $type = null): void`

[](#sendstring-identifier-string-type--null-void)

Generates and queues an OTP notification to the given identifier (non-blocking).

**Parameters:**

- `$identifier` (string): Email, phone, username, or any identifier
- `$type` (string|null): Optional. Identifier type for validation/normalization

**Returns:**

- `void`

**Throws:**

- `RateLimitExceededException`: If rate limit is exceeded
- `InvalidIdentifierException`: If security check fails or type validation fails
- `OtpGenerationException`: If OTP generation/sending fails

**Examples:**

```
try {
    $otp->send('user@example.com', 'email');    // Email with validation
    $otp->send('01700000000', 'sms');           // Phone with SMS type
    $otp->send('username123');                  // No validation
} catch (RateLimitExceededException $e) {
    // Return HTTP 429 with retry_after header
}
```

---

### `sendNow(string $identifier, ?string $type = null): void`

[](#sendnowstring-identifier-string-type--null-void)

Generates and sends an OTP synchronously to the given identifier (blocks until sent).

**Parameters:**

- `$identifier` (string): Email, phone, username, or any identifier
- `$type` (string|null): Optional. Identifier type for validation/normalization

**Returns:**

- `void`

**Throws:**

- `RateLimitExceededException`: If rate limit is exceeded
- `InvalidIdentifierException`: If security check fails or type validation fails
- `OtpGenerationException`: If OTP generation/sending fails

---

### `verify(string $identifier, string $code, ?string $type = null): bool`

[](#verifystring-identifier-string-code-string-type--null-bool)

Verifies an OTP code for the given identifier.

**Parameters:**

- `$identifier` (string): Email, phone, username, or any identifier
- `$code` (string): The OTP code to verify (default 6 digits)
- `$type` (string|null): Optional. Must match the type used in `send()` for normalization consistency

**Returns:**

- `true`: OTP verified successfully
- `false`: Verification failed (invalid, expired, max attempts exceeded, etc.)

**Important:** The `$type` parameter must match what was used when sending the OTP to ensure proper normalization.

**Examples:**

```
$verified = $otp->verify('user@example.com', '123456', 'email');
$verified = $otp->verify('01700000000', '123456', 'sms');  // Same type as send()
```

---

### `addType(string $name, OtpIdentifierType $type): void`

[](#addtypestring-name-otpidentifiertype-type-void)

Register a custom identifier type for validation and normalization.

**Parameters:**

- `$name` (string): Type name (e.g., 'sms', 'email', 'username')
- `$type` (OtpIdentifierType): Type implementation

**Example:**

```
SecureOtpService::addType('sms', new BangladeshSmsType());
```

---

### `cleanupExpired(): int`

[](#cleanupexpired-int)

Deletes expired OTP records older than configured hours.

**Returns:** Number of deleted records

Testing
-------

[](#testing)

The package includes comprehensive tests:

```
composer test
```

Run tests with coverage (requires PCOV or Xdebug):

```
composer test-coverage
```

The package maintains **100% code coverage** with 103 comprehensive tests covering all security features, edge cases, and error scenarios.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Contributions are welcome! Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

If you discover a security vulnerability, please send an email to . All security vulnerabilities will be promptly addressed.

Credits
-------

[](#credits)

- [Md Ashiquzzaman](https://github.com/ashiquzzaman33)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

45

—

FairBetter than 92% of packages

Maintenance79

Regular maintenance activity

Popularity23

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 83.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

2

Last Release

191d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/617e282937940bb12beba78d33f648dc8bb93c0de3f4edd11d81a6ce9371263b?d=identicon)[ashiquzzaman33](/maintainers/ashiquzzaman33)

---

Top Contributors

[![ashiquzzaman33](https://avatars.githubusercontent.com/u/5571291?v=4)](https://github.com/ashiquzzaman33 "ashiquzzaman33 (10 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

laravelbiponixlaravel-secure-otp

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/biponix-laravel-secure-otp/health.svg)

```
[![Health](https://phpackages.com/badges/biponix-laravel-secure-otp/health.svg)](https://phpackages.com/packages/biponix-laravel-secure-otp)
```

###  Alternatives

[spatie/laravel-permission

Permission handling for Laravel 12 and up

12.9k89.8M1.0k](/packages/spatie-laravel-permission)[bezhansalleh/filament-shield

Filament support for `spatie/laravel-permission`.

2.8k2.9M88](/packages/bezhansalleh-filament-shield)[jeffgreco13/filament-breezy

A custom package for Filament with login flow, profile and teams support.

1.0k1.7M41](/packages/jeffgreco13-filament-breezy)[spatie/laravel-login-link

Quickly login to your local environment

4381.2M1](/packages/spatie-laravel-login-link)[ryangjchandler/laravel-cloudflare-turnstile

A simple package to help integrate Cloudflare Turnstile.

438896.6k2](/packages/ryangjchandler-laravel-cloudflare-turnstile)[spatie/laravel-passkeys

Use passkeys in your Laravel app

444494.4k16](/packages/spatie-laravel-passkeys)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
