PHPackages                             horlerdipo/simple-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. horlerdipo/simple-otp

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

horlerdipo/simple-otp
=====================

A OTP implementation for Laravel

v0.1.0(9mo ago)330[4 PRs](https://github.com/Horlerdipo/simple-otp/pulls)MITPHPPHP ^8.1CI passing

Since Jul 19Pushed 1mo ago1 watchersCompare

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

READMEChangelogDependencies (12)Versions (6)Used By (0)

A Simple OTP Package for Laravel
================================

[](#a-simple-otp-package-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/f8193435fb6cf9e301f3560f56bc8b04120b25940da52fc4a5888794b1bdb127/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f686f726c65726469706f2f73696d706c652d6f74702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/horlerdipo/simple-otp)[![GitHub Tests Action Status](https://camo.githubusercontent.com/32c9936f0288bf9d3ac54dc892862b004d7c7c4cab741b35d7a4904433b52b03/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f686f726c65726469706f2f73696d706c652d6f74702f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/horlerdipo/simple-otp/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/06e612deb99c16ea1fdde0368b31919a440d532d8407aa7c16ed2b961c5396fd/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f686f726c65726469706f2f73696d706c652d6f74702f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/horlerdipo/simple-otp/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/6135a4e7b0a7e7d3e3752fefc64f94797b8c088b45e3cd75ea5f19e2a537f31f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f686f726c65726469706f2f73696d706c652d6f74702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/horlerdipo/simple-otp)

This Laravel package provides a flexible and pluggable One-Time Password (OTP) system, supporting multiple delivery channels like Email and custom drivers.

Introduction
------------

[](#introduction)

### Overview

[](#overview)

This Laravel package provides a flexible and pluggable One-Time Password (OTP) system, supporting multiple delivery channels like Email and custom drivers. You can get up and running with a full OTP system with just a couple of lines

### Features

[](#features)

- OTP generation and validation
- Facade support for simple usage
- Built-in Email and BlackHole channels
- Easy integration of custom delivery channels
- Coming soon: Twilio, Termii SMS channels, Redis Storage for OTPs
- Coming soon: TOTP (Time-based One-Time Password)

### Requirements

[](#requirements)

- PHP 8.1+
- Laravel 9.x or higher

### Installation

[](#installation)

You can install the package via composer:

```
composer require horlerdipo/simple-otp
```

You can publish and run the migrations with:

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

php artisan migrate
```

You can publish the config file with:

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

This is the contents of the published config file:

```
return [
    'length' => env('OTP_LENGTH', 6),

    'default_channel' => ChannelType::EMAIL->value,

    'expires_in' => env('OTP_EXPIRATION_TIME', 10),

    'hash' => false,

    'email_template_location' => 'vendor.simple-otp.mails.otp',

    'numbers_only' => true,

    'table_name' => 'otps',

    'messages' => [
        'incorrect_otp' => 'This OTP is incorrect',
        'used_otp' => 'This OTP has already been used',
        'expired_otp' => 'This OTP has expired',
        'valid_otp' => 'This OTP is correct',
    ],
];
```

If you would like to use the package email template(you shouldn't 😂), you can publish the views using

```
php artisan vendor:publish --tag="simple-otp-views"
```

Quickstart
----------

[](#quickstart)

### Basic Setup

[](#basic-setup)

After installation, make sure your .env and config/otp.php are configured correctly, the default channel is Email so OTPs will be sent to emails. You can change the default channel on the config file at runtime as well.

### Sending an OTP

[](#sending-an-otp)

```
use Horlerdipo\SimpleOtp\Facades\SimpleOtp;
SimpleOtp::send(destination: "test@laravel.com", purpose: "login", queue: "default");
```

### Verifying an OTP

[](#verifying-an-otp)

```
use Horlerdipo\SimpleOtp\Facades\SimpleOtp;
$response = SimpleOtp::verify(destination: "test@laravel.com", purpose: "login", token: "267799");
```

The `verify()` method returns a `VerifyOtpResponse` object that has a `status` which is a boolean that is true if the OTP is correct and false if it is not, the object also has `message` property that contains the reason why the OTP is not correct. If for any reason, you would like the otp not to be used immediately, you can pass `['use' => false]` as the fourth parameter for the `verify()`endpoint.

```
use Horlerdipo\SimpleOtp\Facades\SimpleOtp;
$response = SimpleOtp::verify(destination: "test@laravel.com", purpose: "login", token: "267799", options: ['use' => false]);
```

### Configuration Overview

[](#configuration-overview)

You can configure OTP generation using method chaining before calling `send()` method

- `length(int $length)` : This is to set the OTP length, default is 6
- `expiresIn(int $minutes)` : This is to set how long the OTP will last, default is 10 minutes
- `numbersOnly(bool $bool)` : This is to set if the generated OTP should contain letters or not, default is false
- `template(string $template)` : This is to set the template that will be used to send the OTP, default is vendor.simple-otp.mails.otp
- `hash(bool $bool)` : This is to set if the OTP should be hashed before it is saved into the database or not, default is false
- `channel(string $channel)` : This is to set the channel that will be used to send the OTP, if `null` is set, default is email
- `channelName()` : This returns the name of the channel in use, this method cannot be chained like the ones above

Example

```
use Horlerdipo\SimpleOtp\Facades\SimpleOtp;

SimpleOtp::channel(\Horlerdipo\SimpleOtp\Enums\ChannelType::EMAIL->value)
    ->template('vendor.simple-otp.mails.otp')
    ->length(6)
    ->expiresIn(1)
    ->numbersOnly()
    ->hash(false)
    ->send(destination: "test@laravel.com", purpose: "testing", queue: "default");
```

Channel Guide
-------------

[](#channel-guide)

### Email Channel

[](#email-channel)

#### Overview

[](#overview-1)

Ensure your mail configuration is properly set in the .env. The email template is defined in config/simple-otp.php.

#### Example Usage

[](#example-usage)

```
use Horlerdipo\SimpleOtp\Facades\SimpleOtp;
SimpleOtp::channel('email')
    ->send(destination: 'test@laravel.com', purpose: 'password_reset', queue: 'email');
```

### BlackHole Channel

[](#blackhole-channel)

#### Overview

[](#overview-2)

This channel was created primarily for testing or development. It simulates OTP sending without actually delivering the OTP. It comes with a `getToken()` method that returns the token that was generated, this is useful in a scenario where you would like to send the OTP in some other way the package is not shipped with, and you do not want to create a custom channel for it.

#### Example Usage

[](#example-usage-1)

```
use Horlerdipo\SimpleOtp\Facades\SimpleOtp;
SimpleOtp::channel('blackhole')
    ->send('test@laravel.com', '2fa');
```

Advanced Usage
--------------

[](#advanced-usage)

### Adding Custom Channels

[](#adding-custom-channels)

#### Creating a custom channel class

[](#creating-a-custom-channel-class)

The custom channel class must implement the `Horlerdipo\SimpleOtp\Contracts\OtpContract` and the `Horlerdipo\SimpleOtp\Contracts\ChannelContract`. You can simply extend the `Horlerdipo\SimpleOtp\Channels\BaseChannel` abstract class to get predefined methods to speed up your custom channel development

#### Registering the channel

[](#registering-the-channel)

The custom service is registered by calling the `Horlerdipo\SimpleOtp\Facades\SimpleOtp::extend()` method in the `register`method of the service provider

```
class AppServiceProvider extends ServiceProvider
{

    public function register(): void
    {
        $this->app->booting(function () {
            \Horlerdipo\SimpleOtp\Facades\SimpleOtp::extend('sms', function () {
                return new SmsChannel(
                    length: config()->get('simple-otp.length'),
                    expiresIn: config()->get('simple-otp.expires_in'),
                    hashToken: config()->get('simple-otp.hash'),
                    template: config()->get('simple-otp.email_template_location'),
                    numbersOnly: config()->get('simple-otp.numbers_only'),
                );
            });
        });
    }
}
```

#### Custom Channel Usage

[](#custom-channel-usage)

The newly registered channel can now be used by either changing the `default_channel` to `sms` or the name added while registering or using it in the channel

```
use Horlerdipo\SimpleOtp\Facades\SimpleOtp;
SimpleOtp::channel('sms')
    ->send('+23470345480896', '2fa');
```

#### Example Channel Implementation

[](#example-channel-implementation)

```
namespace App\Channels;

use Horlerdipo\SimpleOtp\Channels\BaseChannel;
use Horlerdipo\SimpleOtp\Contracts\ChannelContract;
use Horlerdipo\SimpleOtp\Contracts\OtpContract;
use Horlerdipo\SimpleOtp\DTOs\VerifyOtpResponse;
use Horlerdipo\SimpleOtp\Exceptions\InvalidOtpLengthException;

class SmsChannel extends BaseChannel implements OtpContract, ChannelContract
{
    public function channelName(): string
    {
        return 'sms';
    }

    /**
     * @throws InvalidOtpLengthException
     */
    public function send(string $destination, string $purpose, array $templateData = [], string $queue = 'default'): void
    {
        $token = $this->generateOtp($this->length, $this->numbersOnly);
        $this->storeOtp(
            destination: $destination, token: $token, purpose: $purpose,
            expiration: $this->expiresIn, hashToken: $this->hashToken
        );

        $this->sendOtpToSms($token);
    }

    public function verify(string $destination, string $purpose, string $token, array $options = []): VerifyOtpResponse
    {
        return $this->verifyOtp(
            destination: $destination,
            token: $token,
            purpose: $purpose,
            use: $options['use'] ?? true
        );
    }

    protected function sendOtpToSms(string $token) {
        dd($token);
    }
}
```

The `verifyOtp()` , `generateOtp()` and `storeOtp()` are already implemented in the abstract class, all you need to be concerned about is the `sendOtpToSms()` method which defines how the OTP will be sent to the user.

### Using the Manager Class directly

[](#using-the-manager-class-directly)

If you are not a fan of Facades, you can also simply call the underlying `Horlerdipo\SimpleOtp\SimpleOtpManager` class directly like below

```
Route::get('/generate-otp', function (\Illuminate\Http\Request $request, \Horlerdipo\SimpleOtp\SimpleOtpManager $otpManager) {
    $otpManager->channel('email')
        ->template('vendor.simple-otp.mails.otp')
        ->hash(false)
        ->numbersOnly()
        ->length(6)
        ->expiresIn(1)
        ->send("test@laravel.com", "login");
});

Route::get('/verify-otp', function (\Illuminate\Http\Request $request, \Horlerdipo\SimpleOtp\SimpleOtpManager $otpManager) {
    return dd($otpManager->verify("test@laravel.com", "login", $request->otp));
});
```

You can as well call the Channel classes directly if you even want to go even lower, we currently have the following channels `\Horlerdipo\SimpleOtp\Channels\Email` and the `\Horlerdipo\SimpleOtp\Channels\BlackHole` classes

### Pruning Old OTPs

[](#pruning-old-otps)

To avoid the `otps` table from getting filled up, you should add the `simple-otp:prune-expired-otp` command to your scheduler. This also takes an input of the hours how far back the expired OTP should be, the default is `24`.

```
    protected function schedule(Schedule $schedule): void
    {
        //this will run daily and delete otp that have expired in the last 24 hours
        $schedule->command('simple-otp:prune-expired-otp')->daily();

        //if you are like me and you prefer classes instead, this will do the same thing as the above
        $schedule->command(PruneExpiredOtpCommand::class)->daily();
    }
```

Troubleshooting &amp; FAQ
-------------------------

[](#troubleshooting--faq)

- OTP not being delivered Check mail config or your custom channel integration

Verify your template paths

- OTP always fails validation Check token expiration and matching

Ensure hashing is consistent between send and verify

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

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

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

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

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

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Umar Oladipo](https://github.com/Horlerdipo)
- [All Contributors](../../contributors)

License
-------

[](#license)

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

###  Health Score

36

—

LowBetter than 82% of packages

Maintenance76

Regular maintenance activity

Popularity11

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 76.9% 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

Unknown

Total

1

Last Release

294d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4ce0cf63fd9e994acc511f86986eb60adc819180f1995d0ae4d37c8b6fb53916?d=identicon)[Nightingale](/maintainers/Nightingale)

---

Top Contributors

[![Horlerdipo](https://avatars.githubusercontent.com/u/37878400?v=4)](https://github.com/Horlerdipo "Horlerdipo (20 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (3 commits)")

---

Tags

laravelsimple otpUmar Oladipo

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/horlerdipo-simple-otp/health.svg)

```
[![Health](https://phpackages.com/badges/horlerdipo-simple-otp/health.svg)](https://phpackages.com/packages/horlerdipo-simple-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)
