horlerdipo / simple-otp
A OTP implementation for Laravel
Fund package maintenance!
Umar Oladipo
Requires
- php: ^8.1
- illuminate/contracts: ^10.0||^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^10.0.0||^9.0.0||^8.22.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/extension-installer: ^1.3||^2.0
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
This package is auto-updated.
Last update: 2025-06-16 01:28:36 UTC
README
This Laravel package provides a flexible and pluggable One-Time Password (OTP) system, supporting multiple delivery channels like Email and custom drivers.
Introduction
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
- 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
- PHP 8.1+
- Laravel 9.x or higher
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
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
use Horlerdipo\SimpleOtp\Facades\SimpleOtp; SimpleOtp::send(destination: "test@laravel.com", purpose: "login", queue: "default");
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
You can configure OTP generation using method chaining before calling send()
method
length(int $length)
: This is to set the OTP length, default is 6expiresIn(int $minutes)
: This is to set how long the OTP will last, default is 10 minutesnumbersOnly(bool $bool)
: This is to set if the generated OTP should contain letters or not, default is falsetemplate(string $template)
: This is to set the template that will be used to send the OTP, default is vendor.simple-otp.mails.otphash(bool $bool)
: This is to set if the OTP should be hashed before it is saved into the database or not, default is falsechannel(string $channel)
: This is to set the channel that will be used to send the OTP, ifnull
is set, default is emailchannelName()
: 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
Email Channel
Overview
Ensure your mail configuration is properly set in the .env. The email template is defined in config/simple-otp.php.
Example Usage
use Horlerdipo\SimpleOtp\Facades\SimpleOtp; SimpleOtp::channel('email') ->send(destination: 'test@laravel.com', purpose: 'password_reset', queue: 'email');
BlackHole Channel
Overview
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
use Horlerdipo\SimpleOtp\Facades\SimpleOtp; SimpleOtp::channel('blackhole') ->send('test@laravel.com', '2fa');
Advanced Usage
Adding Custom Channels
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
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
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
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
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
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 & 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
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.