PHPackages                             prasanth-j/otpify - 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. prasanth-j/otpify

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

prasanth-j/otpify
=================

A secure, flexible OTP package for Laravel. Supports database and cache drivers, multiple purposes, hashed storage, events, and validation rules.

v2.0.0(1mo ago)740MITPHPPHP ^8.1

Since Aug 5Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/prasanth-j/otpify)[ Packagist](https://packagist.org/packages/prasanth-j/otpify)[ Docs](https://github.com/prasanth-j/otpify)[ RSS](/packages/prasanth-j-otpify/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (5)Dependencies (13)Versions (8)Used By (0)

Otpify
======

[](#otpify)

[![Build Status](https://camo.githubusercontent.com/7e83940173064b2b4e6a06f856203bc048e767ad969d170b97222735c06fbf79/68747470733a2f2f7363727574696e697a65722d63692e636f6d2f672f70726173616e74682d6a2f6f74706966792f6261646765732f6275696c642e706e673f623d6d6173746572)](https://scrutinizer-ci.com/g/prasanth-j/otpify/build-status/master)[![Total Downloads](https://camo.githubusercontent.com/61b65334b1745f9968445b72ac23ef7fef74ce83bd8b7efeed7f72f6cc34e02e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f70726173616e74682d6a2f6f74706966792e737667)](https://packagist.org/packages/prasanth-j/otpify)[![Latest Version on Packagist](https://camo.githubusercontent.com/13df4db2cdc3936962dcf3081ed0a16fc57c6e1631c2354993fa1bcfbe786409/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f70726173616e74682d6a2f6f7470696679)](https://packagist.org/packages/prasanth-j/otpify)[![License: MIT](https://camo.githubusercontent.com/4cee3b0c35d9c4bb7d64c3ea0842daf10714208f10e24bddaaa48d319c845ed4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f70726173616e74682d6a2f6f7470696679)](https://opensource.org/licenses/MIT)

A secure, flexible OTP (One-Time Password) package for Laravel. Supports database and cache storage drivers, multiple purposes per identifier, hashed token storage, events, and a ready-made validation rule.

Requirements
------------

[](#requirements)

- PHP 8.1+
- Laravel 9, 10, 11, 12, or 13

Each Laravel major has its own PHP range and support window (per Laravel's official support policy), so the actual floor depends on which Laravel version you're on:

LaravelPHP rangeSecurity fixes until98.0 – 8.2 (this package requires 8.1+)Feb 2024 — EOL108.1 – 8.3Feb 2025 — EOL118.2 – 8.4Mar 2026 — EOL128.2 – 8.5Feb 2027138.3 – 8.5Mar 2028Laravel 9, 10, and 11 no longer receive security fixes upstream. This package still works on them, but for new projects prefer Laravel 12 or 13.

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

[](#installation)

```
composer require prasanth-j/otpify
```

Publish the config file:

```
php artisan vendor:publish --tag=otpify-config
```

If you plan to use the `database` driver (the default), publish and run the migration:

```
php artisan vendor:publish --tag=otpify-migrations
php artisan migrate
```

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

[](#configuration)

`config/otpify.php`:

```
return [
    'driver'   => env('OTPIFY_DRIVER', 'database'),
    'digits'   => env('OTPIFY_DIGITS', 6),
    'validity' => env('OTPIFY_VALIDITY', 10), // minutes
    'type'     => env('OTPIFY_TYPE', 'numeric'), // numeric|alpha|alphanumeric
    'cache'    => [
        'prefix' => 'otpify',
        'store'  => env('OTPIFY_CACHE_STORE', null), // null = default cache store
    ],
];
```

KeyDescription`driver``database` or `cache`.`digits`Default OTP length (4-8). Can be overridden per call.`validity`Default expiry time in minutes. Can be overridden per call.`type`Default character set: `numeric`, `alpha`, or `alphanumeric`.`cache.prefix`Prefix used to build cache keys (`otpify:{identifier}:{purpose}`).`cache.store`Which cache store to use. `null` uses your app's default store.Security
--------

[](#security)

- OTPs are generated with `random_int()`, never `mt_rand()`.
- Only the SHA-256 hash of the OTP is stored (`hash('sha256', $token)`) — the plaintext is never persisted.
- Validation compares hashes with `hash_equals()` for constant-time comparison.
- The plaintext token is only ever returned once, from `generate()`/`resend()`, so you can send it via email/SMS/etc. `validate()` never returns the token.
- OTPs are single-use: a successful `validate()` call immediately invalidates the OTP.

Usage
-----

[](#usage)

### Generate an OTP

[](#generate-an-otp)

```
use PrasanthJ\Otpify\Facades\Otpify;

$result = Otpify::generate('user@example.com'); // purpose defaults to "default"

$result->token;     // e.g. "482913" — send this to the user
$result->expiresAt; // Carbon instance
$result->status;    // "generated"
```

With a purpose and custom options:

```
$result = Otpify::generate('user@example.com', 'login', [
    'digits'   => 4,
    'validity' => 5,          // minutes
    'type'     => 'alphanumeric',
]);
```

Generating a new OTP for the same identifier + purpose deletes any previous one — only the latest OTP for a given identifier/purpose is ever valid.

### Validate an OTP

[](#validate-an-otp)

```
$result = Otpify::validate('user@example.com', $token, 'login');

if ($result->isValid()) {
    // proceed
}
```

`OtpResult` status values and helpers:

StatusHelperMeaning`valid``isValid()`Token matched, unused, not expired.`invalid``isInvalid()`Token does not match.`expired``isExpired()`Token matched an OTP that has expired.`already_used``isAlreadyUsed()`OTP was already validated once.`not_found`—No OTP exists for that identifier/purpose.`generated``wasGenerated()`Returned only by `generate()`/`resend()`.### Invalidate an OTP

[](#invalidate-an-otp)

```
Otpify::invalidate('user@example.com', 'login'); // returns bool
```

### Resend an OTP

[](#resend-an-otp)

Invalidates any existing OTP for the identifier/purpose and generates a fresh one:

```
$result = Otpify::resend('user@example.com', 'login', ['validity' => 10]);
```

### Multiple purposes

[](#multiple-purposes)

Every method accepts a `purpose` so the same identifier can hold independent OTPs at once, e.g. `'login'`, `'2fa'`, `'password-reset'`.

```
Otpify::generate('user@example.com', 'login');
Otpify::generate('user@example.com', '2fa');
```

Validation rule
---------------

[](#validation-rule)

Use `OtpRule` to validate an OTP as part of a normal Laravel form request or validator:

```
use Illuminate\Support\Facades\Validator;
use PrasanthJ\Otpify\Rules\OtpRule;

$validator = Validator::make($request->all(), [
    'otp' => ['required', new OtpRule($request->input('email'), 'login')],
]);
```

Events
------

[](#events)

EventFired when`PrasanthJ\Otpify\Events\OtpGenerated`An OTP is generated. Has `identifier`, `purpose`, `token`, `expiresAt`.`PrasanthJ\Otpify\Events\OtpValidated`An OTP passes validation. Has `identifier`, `purpose`.`PrasanthJ\Otpify\Events\OtpFailed`An OTP fails validation. Has `identifier`, `purpose`, `reason` (the failure status).```
use PrasanthJ\Otpify\Events\OtpGenerated;

Event::listen(function (OtpGenerated $event) {
    // send $event->token to $event->identifier via SMS/email
});
```

Drivers
-------

[](#drivers)

### Database (default)

[](#database-default)

Stores hashed OTPs in the `otpify_tokens` table. Requires the migration to be run. Good default choice — durable, works with the `otpify:clean` command.

### Cache

[](#cache)

Stores hashed OTPs in your configured cache store under `otpify:{identifier}:{purpose}`, using the OTP's expiry as the cache TTL. No migration needed; good fit if you already run Redis/Memcached and don't need a durable audit trail.

```
OTPIFY_DRIVER=cache
OTPIFY_CACHE_STORE=redis
```

Rate limiting
-------------

[](#rate-limiting)

Otpify does not rate limit generation/validation attempts itself — pair it with Laravel's `RateLimiter`:

```
use Illuminate\Support\Facades\RateLimiter;
use PrasanthJ\Otpify\Facades\Otpify;

$key = 'otp-generate:' . $request->ip() . ':' . $request->input('email');

if (RateLimiter::tooManyAttempts($key, 5)) {
    abort(429, 'Too many OTP requests. Please try again later.');
}

RateLimiter::hit($key, 60); // 1 attempt per minute window, 5 max

$result = Otpify::generate($request->input('email'));
```

Do the same around `Otpify::validate()` to slow down brute-force guessing attempts.

Cleaning up expired tokens
--------------------------

[](#cleaning-up-expired-tokens)

For the `database` driver, expired and used rows accumulate over time. Clean them up with:

```
php artisan otpify:clean
```

Schedule it in `routes/console.php` (Laravel 11+) or `app/Console/Kernel.php` (Laravel 9/10):

```
Schedule::command('otpify:clean')->daily();
```

This command is a no-op (with a warning) when the `cache` driver is active, since cache entries expire on their own via TTL.

Migrating from v1
-----------------

[](#migrating-from-v1)

v2.0.0 is a full rewrite and breaking change:

- The `otps` table and `Otp` model are gone, replaced by the `otpify_tokens` table (no model).
- `Otpify::generate()`/`validate()` now return an `OtpResult` object instead of an array.
- `Otpify::generate()` signature changed from `generate(string $identifier, int $userId = null, string $otpType = null, int $digits = null, int $validity = null)`to `generate(string $identifier, string $purpose = 'default', array $options = [])`.
- `otpType` is now `purpose`, and is a required-shaped concept everywhere (defaults to `'default'`).
- OTPs are now stored hashed (SHA-256) instead of in plaintext.
- `user_id` is no longer stored — key OTPs by whatever identifier makes sense for you (email, phone, user ID as a string, etc.).

If you have existing data in the old `otps` table, write a one-off migration to hash and copy `identifier`/`otp_type`/`token`/expiry data into `otpify_tokens`, or simply let outstanding OTPs expire naturally and drop the old table.

Development
-----------

[](#development)

```
composer test      # run the Pest test suite
composer lint       # fix code style with Laravel Pint
composer lint:test  # check code style without fixing
composer analyse    # run static analysis with Larastan
```

Larastan 3.x requires PHP 8.2+ locally to run static analysis, even though the package itself supports PHP 8.1+.

License
-------

[](#license)

MIT

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance90

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor1

Top contributor holds 100% 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 ~356 days

Total

5

Last Release

46d ago

Major Versions

v1.0.3 → v2.0.02026-07-02

PHP version history (5 changes)v1.0.0PHP ^7.4

v1.0.1PHP &gt;=5.4.0

v1.0.2PHP &gt;=7.4.0

v1.0.3PHP ^8.0

v2.0.0PHP ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/85541084b54c4232d09cfd2b861299f5b505e5ce991193521758323489be6c64?d=identicon)[prasanthjayakumar](/maintainers/prasanthjayakumar)

---

Top Contributors

[![prasanth-j](https://avatars.githubusercontent.com/u/29867996?v=4)](https://github.com/prasanth-j "prasanth-j (18 commits)")

---

Tags

laravellaravel-otplaravel-packageone-time-passwordotp-verificationotpifyone-time-passwordlaravel otpotp-validationotpify

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/prasanth-j-otpify/health.svg)

```
[![Health](https://phpackages.com/badges/prasanth-j-otpify/health.svg)](https://phpackages.com/packages/prasanth-j-otpify)
```

###  Alternatives

[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k161.4k2](/packages/mike-bronner-laravel-model-caching)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k31.8M160](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k16.3M154](/packages/laravel-pulse)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M315](/packages/laravel-ai)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

265.2k](/packages/aedart-athenaeum)[flarum/core

Delightfully simple forum software.

211.5M2.5k](/packages/flarum-core)

PHPackages © 2026

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