salehhashemi/laravel-otp-manager
Laravel OTP Manager provides a simple, secure way to generate, send, and verify one-time passwords in Laravel applications. It supports multiple delivery channels, configurable expiry, rate limiting, and middleware to protect routes and actions with minimal setup.
Install the package and let Laravel discover it:
composer require salehhashemi/laravel-otp-manager
php artisan vendor:publish --tag=otp-manager-config
php artisan migrate
The published config/otp-manager.php is the first file to read. It controls the code length, expiry, resend interval, max send attempts, and the notification class used to deliver the code. If those defaults don't match your product (most apps want 4–6 digits, 2–5 minute expiry, a 60-second resend cooldown), change them before writing any application code.
The package ships a facade as the primary entry point:
use Salehhashemi\OtpManager\Facades\OtpManager;
$sent = OtpManager::send($mobile);
$valid = OtpManager::verify($mobile, $request->code);
The first use case to wire up is passwordless login or phone verification: send an OTP to a phone number/email, verify it, then issue a session or Sanctum token. Do that end-to-end in a throwaway route before layering it into real auth flows, because it exercises the config, notification, and database table all at once.
Look at the package's Notifications directory next — that is where channel and template customization lives, and it's the extension point you will touch most often.
Wrap the facade in your own service. Don't call OtpManager directly from controllers. Create an OtpService or action classes (SendOtp, VerifyOtp) so you can normalize identifiers, add context (login vs. phone verification vs. password reset), and test without the facade:
final class SendLoginOtp
{
public function __invoke(string $mobile): void
{
$mobile = PhoneNumber::normalize($mobile); // always normalize first
OtpManager::send($mobile);
}
}
Pair verification with token issuance. The verify call returns a boolean, so the controller pattern is: validate the request, call verify, and on true issue $user->createToken(...) or Auth::login($user). Never log the user in before verification succeeds, and clear any pending OTP state on success.
Customize the notification, not the package. The config exposes a notification class. Subclass or replace it to control the SMS/email channel, the message body, and localization. This is the cleanest way to brand the message and to route through your existing SMS driver instead of adding a second provider.
Use it as a model concern where it fits. If the package provides a trait for authenticatable models, use it for $user->sendOtp() style calls; otherwise keep the identifier-based facade API and treat OTPs as a separate concern from the user record. Mixing both approaches in one codebase gets confusing fast.
Handle resend explicitly. Treat "resend" as a distinct endpoint with its own throttle middleware. The package's send-interval config gives you a baseline, but a route-level throttle: rule is what actually stops abuse from a scripted client.
Test with notification fakes and config overrides. In feature tests, Notification::fake() and assert the OTP notification was sent, then read the code from the notification or override the code length/expiry in the test. For unit tests, bind a fake around your own OtpService rather than the vendor facade.
Separate OTP purposes if your product needs it. Login codes and "verify your new phone number" codes should not be interchangeable. If the package keys only on the identifier, namespace it (e.g. prefix the identifier with the purpose) or keep a small application-level table that records the intent.
now() in the app timezone. Write one test that travels time past the expiry and asserts failure.otps table as sensitive: short expiry, delete-on-use, and no code values in logs or exception traces. If you need hashed storage, that's a fork or a custom model, not a config toggle.+1 (555) 010-9999 and 15550109999 must resolve to the same database row, or users get multiple live codes and confusing failures. Normalize in one place, at the edge.ShouldQueue, a missing queue worker means "OTP sent" responses with no code arriving. Either run the worker in every environment or force the notification to send synchronously.send().How can I help you explore Laravel packages today?