Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Otp Laravel Package

fouladgar/laravel-otp

A Laravel package for generating, sending, and verifying one-time passwords. It provides configurable OTP length and expiry, resend and rate limits, customizable notification channels, and straightforward integration with login, registration, and password reset flows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config/migration:

composer require fouladgar/laravel-otp
php artisan vendor:publish --provider="Fouladgar\OTP\OTPServiceProvider"
php artisan migrate

If the provider publish command does not find anything, run php artisan vendor:publish and select the OTP provider/tags manually.

Look first at:

  • config/otp.php — token length, expiry, storage/notification settings.
  • The published migration/table — typically the otps table used to persist recipients, tokens, and expiry.
  • src/OTP.php and the service provider — confirm the exact facade methods and container bindings for your installed version.
  • The README examples — the facade API is small, so a quick read is faster than guessing method signatures.

First use case: phone/email verification on registration or passwordless login. The common flow is a “send code” endpoint followed by a “verify code” endpoint:

use Fouladgar\OTP\Facades\OTP;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

Route::post('/otp/send', function (Request $request) {
    $data = $request->validate([
        'mobile' => ['required', 'regex:/^09\d{9}$/'],
    ]);

    OTP::send($data['mobile']);

    return response()->noContent();
});

Route::post('/otp/verify', function (Request $request) {
    $data = $request->validate([
        'mobile' => ['required', 'regex:/^09\d{9}$/'],
        'token'  => ['required', 'string'],
    ]);

    if (! OTP::validate($data['mobile'], $data['token'])) {
        throw ValidationException::withMessages([
            'token' => 'The code is invalid or has expired.',
        ]);
    }

    // Mark the phone as verified, log the user in, or issue a Sanctum token.
    return response()->noContent();
});

Protect both endpoints with Laravel throttling before exposing them publicly.

Implementation Patterns

Centralize OTP behavior in one application service. Avoid scattering OTP::send() and OTP::validate() across controllers. Wrap them so recipient normalization, throttling, logging, purpose scoping, and auth side effects stay consistent.

class OtpService
{
    public function send(string $mobile): void
    {
        OTP::send($this->normalize($mobile));
    }

    public function verify(string $mobile, string $token): bool
    {
        return OTP::validate($this->normalize($mobile), $token);
    }

    private function normalize(string $mobile): string
    {
        return preg_replace('/\D+/', '', $mobile);
    }
}

Normalize the recipient before both send and validate. The most common production bug is sending to +98912... and validating 0912..., or trimming/lowercasing email in one place but not the other. Pick one canonical representation and store/use it everywhere.

Throttle sends and verification attempts separately. A five-digit OTP has a small search space; verification throttling is not optional.

use Illuminate\Support\Facades\RateLimiter;

$key = 'otp-send:'.sha1($mobile);

$allowed = RateLimiter::attempt($key, 3, function () use ($mobile) {
    OTP::send($mobile);
}, 60);

if (! $allowed) {
    abort(429, 'Please wait before requesting another code.');
}

For verification:

$key = 'otp-verify:'.sha1($mobile);

if (RateLimiter::tooManyAttempts($key, 5)) {
    abort(429, 'Too many attempts.');
}

if (! OTP::validate($mobile, $token)) {
    RateLimiter::hit($key, 60);
    throw ValidationException::withMessages(['token' => 'Invalid code.']);
}

RateLimiter::clear($key);

Separate OTP purposes if your app has more than one flow. If you use OTPs for login, phone verification, and password reset, do not let a login code silently verify a phone number or reset a password. If the package only keys by recipient, either prefix the recipient in your own layer (login:0912..., verify:0912...) or keep a separate application-level purpose/token record and validate both.

Integrate with authentication only after verification. Once OTP::validate() succeeds, perform the explicit side effect: mark phone_verified_at, call Auth::login($user), or issue a Sanctum token. Treat the OTP as proof of control of the recipient, not as a full session by itself.

Use the package notification/sender extension points for delivery. For local development, route OTPs to logs or a fake sender rather than a real SMS provider. For production, either configure the package’s notification/channel settings or bind/override its sender contract in a service provider so your SMS provider is used consistently.

Queue delivery carefully. If you customize the notification to implement ShouldQueue, ensure a worker is running and that the token expiry starts at generation time, not delivery time. Also decide what happens if delivery fails: should the token be revoked, or can the user request a new one?

Test with fake notifications and frozen time. Use Notification::fake() or a fake sender binding, then assert that a notification was sent and that validation succeeds/fails as expected. Use Carbon::setTestNow() to test expiry rather than sleeping.

Notification::fake();

OTP::send('09120000000');

Notification::assertSentOnDemand(OTPNotification::class);

$this->assertTrue(OTP::validate('09120000000', $token));

If the package exposes a generate-only method, use that in tests to get the token directly; otherwise read the token from the configured store after sending.

Gotchas and Tips

  • Clear config cache after changing config/otp.php. In local development run php artisan config:clear; in production rebuild with php artisan config:cache. A cached old expiry or token length causes confusing test results.
  • Verify units for expiry. Confirm whether the config value is seconds or minutes in your installed version. Write an expiry test with Carbon::setTestNow() rather than assuming.
  • Do not log OTPs. Request logging, exception handlers, and API debug output can capture the token field. Redact it and never return the generated token in an HTTP response.
  • Assume tokens are sensitive in storage. Check the published migration/source to see whether tokens are hashed and whether expired rows are cleaned up. If tokens are stored plaintext, restrict database access and add cleanup/indexes.
  • Add indexes and cleanup. Index the recipient and expiry columns. If the package does not ship a cleanup command, schedule deletion of expired rows to keep the table small.
  • Beware concurrent sends. Two rapid send requests may create two valid codes or invalidate the first unexpectedly. Use a cache lock or rate limiter around send, and decide explicitly whether “last code wins” or “first code remains valid.”
  • Do not reveal whether a recipient exists. For login/reset flows, return a generic message such as “If this number is registered, a code has been sent.” Otherwise the endpoint becomes an account enumeration tool.
  • Validation may throw instead of returning false. Check the package source/README for the installed version and catch the correct exception type if needed. Do not rely on a boolean return unless you have confirmed it.
  • Pin the package version and check Laravel compatibility. This is a small package with limited dependent ecosystem validation; review the source and run your own integration tests before relying on it for critical auth flows.
  • Scope OTPs by tenant/guard when relevant. In multi-tenant or multi-guard applications, the recipient string alone may not be enough. Include tenant/user/guard context in your own wrapper key or validation checks.
  • Extension points to inspect: published config, the service provider bindings, the notification class, and the sender contract. These are usually the cleanest places to swap SMS providers, customize messages, or use email instead of SMS.
  • Database table changes require a new migration. If you rename the configured OTP table after the initial migration, add a migration and update config rather than editing the published migration in place on an existing environment.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package