fouladgar/laravel-otp
| Install | |
|---|---|
composer require fouladgar/laravel-otp |
|
| Latest Version: | v6.0.1 |
| PHP: | ^8.2 |
| License: | MIT |
| Last Updated: | Aug 9, 2026 |
| Links: | GitHub · Packagist |
Laravel OTP (One-Time Password)
Upgrading from v5? See the Upgrade Guide for a step-by-step migration walkthrough.
Introduction
Laravel OTP provides a secure, simple, and flexible way to generate, send, and validate One-Time Passwords. It supports multiple independent OTP flows through purpose-based tokens, ensuring that another cannot consume a token generated for one flow. OTP delivery is fully extensible and can use any notification channel, including SMS, email, or custom channels. The package is event-driven, providing lifecycle events that make it easy to integrate OTP flows with your application's business logic, monitoring, auditing, or external notification services.
Table of Contents
- Basic Usage
- Requirements
- Installation
- Configuration
- Purpose
- Disabling Notifications
- Customization
- Events
- Translations
- Testing
Basic Usage
Once the package is configured, sending and validating tokens is straightforward. The same API works whether the recipient is a mobile number, an email address, or another identifier supported by your notification channel.
Send a token:
use Fouladgar\OTP\Facades\OTP;
OTP::send('+98900000000');
Send to an email address (point otp.channel to mail):
OTP::channel('mail')->send('user@example.com');
Send through multiple channels for this call:
OTP::channel(['mail', \App\Channels\CustomChannel::class])
->send('+98900000000');
Validate a token (recipient must match the value used for send()):
OTP::validate('+98900000000', 'token_123'); // returns bool
Read the generated token after send():
use Fouladgar\OTP\Facades\OTP;
OTP::send('+98900000000');
$token = OTP::getToken();
Revoke a pending token:
OTP::revoke('+98900000000');
send()andvalidate()returnbool. On failure, the package throwsFouladgar\OTP\Exceptions\OTPExceptioninstead of returningfalse.
Every channel receives the same recipient value. Only combine channels that understand the same recipient format.
For example, do not mix mail with SMS-only channels unless the recipient is an email address.
You can also inject Fouladgar\OTP\OTPBroker directly wherever you need it, such as in a controller or service
class. The OTP() helper is a shortcut for app(OTPBroker::class) and returns the broker directly — useful when
you need to chain calls without a static import.
Requirements
- PHP
^8.2 - Laravel
^10.0 | ^11.0 | ^12.0 | ^13.0
Installation
Install the package with Composer:
composer require fouladgar/laravel-otp
Thanks to Laravel's package auto-discovery, the service provider is registered automatically. No manual provider registration is required.
Configuration
Next, publish the configuration file. This creates config/otp.php, where you can choose the notification channel,
token storage driver, lifetime, and token length.
php artisan vendor:publish --provider="Fouladgar\OTP\ServiceProvider" --tag="config"
Notification Channel
The package generates the token, builds a Fouladgar\OTP\Notifications\OTPNotification, and dispatches it through
the channel configured in otp.channel. The default channel is otp_log, which writes tokens to Laravel's log —
useful during development. Replace it with your real channel before going to production.
Write a standard Laravel notification channel and point otp.channel to it:
<?php
namespace App\Channels;
use Fouladgar\OTP\Notifications\OTPNotification;
use Fouladgar\OTP\Notifications\Messages\OTPMessage;
use Fouladgar\OTP\Notifications\Messages\MessagePayload;
class MySMSChannel
{
public function send($notifiable, OTPNotification $notification): void
{
$message = $notification->toSMS($notifiable);
// $message->getPayload()->to() → recipient
// $message->getPayload()->content() → token message
}
}
// config/otp.php
return [
// ...
'channel' => \App\Channels\MySMSChannel::class,
];
The recipient passed to
send()is used as the routing target for every configured channel through Laravel's on-demand notifications. Built-in channels such asOTP::channel('mail')->send('info@example.com').
Token Storage
The package stores generated tokens in either cache or database. The default driver is cache, which is usually
enough for simple applications.
You can change the driver in config/otp.php:
// config/otp.php
return [
/*
|--------------------------------------------------------------------------
| Supported drivers: "cache", "database"
|--------------------------------------------------------------------------
*/
'token_storage' => 'cache',
];
Cache
The cache driver uses your application's configured Laravel cache store. If your app does not have a suitable
cache store, use the database driver.
Database
The database driver stores tokens in a table created by the package migration.
Customize the table name if needed:
// config/otp.php
return [
'token_table' => 'otp_token',
// ...
];
Then run the migrations:
php artisan migrate
Token Lifetime
token_lifetime controls how long a pending token remains valid. While the current token has not expired, the
package will not generate or send another token for the same recipient and purpose.
This prevents users from requesting several active codes for the same flow at the same time.
// config/otp.php
return [
'token_lifetime' => env('OTP_TOKEN_LIFE_TIME', 2),
// ...
];
Token Length
token_length controls how many characters are generated for each token. The default value is 5, but you can
increase or decrease it based on your application's verification requirements.
// config/otp.php
return [
// ...
'token_length' => env('OTP_TOKEN_LENGTH', 5),
// ...
];
Token Generator
token_generator controls how a token's value is generated. By default, NumericAbstractTokenGenerator produces
digits-only tokens, which works well for SMS delivery on a numeric keypad.
// config/otp.php
return [
// ...
'token_generator' => Fouladgar\OTP\Token\Generators\NumericAbstractTokenGenerator::class,
// ...
];
For channels where higher entropy per character is worth it — email, for example — swap in the bundled
AlphanumericAbstractTokenGenerator, which generates uppercase letters and digits:
'token_generator' => Fouladgar\OTP\Token\Generators\AlphanumericAbstractTokenGenerator::class,
You can also provide your own class extending Fouladgar\OTP\Contracts\AbstractTokenGenerator:
use Fouladgar\OTP\Contracts\AbstractTokenGenerator;
class MyTokenGenerator extends AbstractTokenGenerator
{
public function generate(int $length): string
{
// (random_int/random_bytes) — never rand() or mt_rand().
}
}
Purpose
Every token is generated for a recipient and a purpose. The purpose is a short scope tag that lets multiple OTP
flows exist for the same recipient without colliding.
For example, a user may request a login token and then request a password reset token before validating the first one. With separate purposes, both tokens can exist independently for the same phone number.
use Fouladgar\OTP\Facades\OTP;
OTP::purpose('login_')->send('+98900000000');
OTP::purpose('password_reset_')->send('+98900000000');
// This does not affect the still-pending "login_" token.
OTP::purpose('password_reset_')->validate('+98900000000', 'token_123');
validate()andrevoke()must use the same purpose that was used bysend().- If you never call
purpose(), the package uses theotp.default_purposeconfig value. The default isotp_. - A pending token only blocks another
send()when both the recipient and purpose are the same.
Disabling Notifications
Use withNotify(false) when this package should generate the token but another part of your system should deliver
it. This is useful when SMS or email delivery is handled by a separate notification service.
In this mode, send() only generates and stores the token. No notification channel is required, and no notification
is dispatched.
use Fouladgar\OTP\Facades\OTP;
OTP::withNotify(false)->send('98900000000');
To disable notifications globally for all calls, set with_notify to false in config/otp.php:
'with_notify' => false,
After calling send(), you can read the token directly with getToken(). In a decoupled setup, listening for
TokenCreated is usually cleaner because a listener can publish the token to a queue or call your notification
service.
<?php
use Fouladgar\OTP\Events\TokenCreated;
use Illuminate\Support\Facades\Event;
// e.g. in App\Providers\EventServiceProvider::boot()
Event::listen(TokenCreated::class, function (TokenCreated $event) {
// publish to your notification microservice, e.g.:
// NotificationServiceClient::send($event->recipient, $event->token);
});
withNotify(false) only affects send(). validate() and revoke() behave the same either way.
Customization
Notification Customization
You can customize the SMS and mail messages by registering closures in your
App\Providers\AppServiceProvider::boot(). Each closure receives the recipient and the generated token.
<?php
use Fouladgar\OTP\Notifications\OTPNotification;
use Fouladgar\OTP\Notifications\Messages\OTPMessage;
use Illuminate\Notifications\Messages\MailMessage;
public function boot()
{
OTPNotification::toSMSUsing(fn ($recipient, $token) => (new OTPMessage())
->to($recipient)
->content('Your OTP Token is: '.$token)
->template('OTP_TEMPLATE'));
OTPNotification::toMailUsing(fn ($recipient, $token) => (new MailMessage)
->subject('OTP Request')
->line('Your OTP Token is: '.$token));
}
Events
OTPBroker dispatches events throughout the token lifecycle. Listen for them with Laravel's normal event tools:
Event::listen(), listener classes, or App\Providers\EventServiceProvider.
Events are useful for audit logs, security monitoring, and decoupled notification delivery. For example, you can
listen for TokenCreated and publish the token to a queue or notification service.
Token Events
Fouladgar\OTP\Events\CreatingToken
- When: before a token is generated
- Payload:
recipient,purpose - Cancelable: yes. If any listener returns
false, token creation stops. No token is stored, no notification is sent, andsend()returnsfalse.
Fouladgar\OTP\Events\TokenCreated
- When: after a token is generated and stored
- Payload:
recipient,token,purpose - Cancelable: no
Fouladgar\OTP\Events\TokenCreationFailed
- When:
send()is blocked because a pending token already exists for the same recipient and purpose - Payload:
recipient,purpose - Cancelable: no
Fouladgar\OTP\Events\TokenValidated
- When:
validate()succeeds, before the token is revoked - Payload:
recipient,purpose - Cancelable: no
Fouladgar\OTP\Events\TokenValidationFailed
- When:
validate()receives a missing, invalid, or expired token - Payload:
recipient,purpose - Cancelable: no
Fouladgar\OTP\Events\TokenRevoked
- When: a pending token is deleted by
revoke()or after a successfulvalidate() - Payload:
recipient,purpose - Cancelable: no
Notification Events
Fouladgar\OTP\Events\SendingNotification
- When: before the built-in notification is dispatched
- Payload:
recipient,token,channels - Cancelable: yes. If any listener returns
false, the token stays stored but the package does not send the notification for this call. - Skipped when: notifications are disabled with
withNotify(false)
Fouladgar\OTP\Events\NotificationSent
- When: after the built-in notification has been handed off
- Payload:
recipient,token,channels - Cancelable: no
- Skipped when: notifications are disabled with
withNotify(false)
TokenCreationFailedandTokenValidationFailedare useful security signals for repeated OTP requests or brute-force attempts. Missing channel configuration is not emitted as an event; it is reported immediately throughFouladgar\OTP\Exceptions\OTPException.
Translations
Publish the translation file with:
php artisan vendor:publish --provider="Fouladgar\OTP\ServiceProvider" --tag="lang"
You can customize the provided language file:
// resources/lang/vendor/OTP/en/otp.php
<?php
return [
'otp_token' => 'Your OTP Token is: :token.',
'otp_subject' => 'OTP request',
];
Testing
Faking a Token in Your Application's Tests
When testing a flow in your own application, such as an OTP login endpoint, you usually need a valid token without actually sending SMS or email.
Use OTP::fake() instead of reaching into the cache or database directly. It stores a real, valid token using your
current configuration, including storage driver and purpose.
<?php
use Fouladgar\OTP\Facades\OTP;
// Generate and store a random token, then use the returned value in your test.
$token = OTP::fake('98900000000');
// Or force a specific value if your test needs to assert on it:
$token = OTP::fake('98900000000', '12345');
// Pass a purpose directly if you are not using the default one.
$token = OTP::fake('98900000000', '12345', 'login_');
$this->postJson('/login-otp', [
'mobile' => '98900000000',
'token' => $token,
]);
fake()skips the "already sent" check and does not dispatch events. It is only a test setup helper, not part of the normalsend()/validate()flow.
Running Tests
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security-related issues, please email fouladgar.dev@gmail.com instead of using the issue tracker.
License
Laravel-OTP is released under the MIT License. See the bundled LICENSE file for details.
Built with ❤️ for you.
Related Packages
A comprehensive WhatsApp integration package for Laravel with dual provider supp...
SuperAuth - The Ultimate Laravel Authentication System with AI-Powered Security,...