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 Verification Code Laravel Package

nextapps/laravel-verification-code

Generate and verify one-time verification codes for Laravel apps. Create numeric or alphanumeric codes, send them via mail or SMS, and validate user input with built-in expiry, attempt limits, and rate limiting—ideal for login, signup, and password reset verification.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish in one pass:

composer require nextapps/laravel-verification-code
php artisan vendor:publish --tag=verification-code   # config + migration
php artisan migrate                                   # verification_codes table

Open the published config/verification-code.php first — that file, not the README, is the source of truth for code length, TTL, attempt limits, and which notification class actually delivers the code. Then read the package's src/ tree (it's small, a handful of classes) before wiring anything into auth.

The minimal first use case is a passwordless login or a registration-time email check. Attach the trait to the model that owns the code:

use NextApps\VerificationCode\Concerns\HasVerificationCodes;

class User extends Authenticatable
{
    use HasVerificationCodes;
}

Then issue and check a code:

$user->generateVerificationCode();          // persists + sends via notification
$user->verifyCode($request->input('code')); // bool

If you don't want the code tied to an Eloquent model — invite flows, pre-registration email checks, phone numbers — use the facade against a plain identifier:

VerificationCode::create('[email protected]');
VerificationCode::verify('[email protected]', $request->input('code'));

Pick one of these two shapes per flow and stick to it. Mixing trait-based and facade-based codes for the same identifier is the fastest way to produce confusing "code doesn't work" bug reports.

Implementation Patterns

Two-request passwordless login. Don't verify and authenticate in the same call. Request-code endpoint creates the code and always returns a generic 200; verify endpoint consumes the code and only then issues a session or Sanctum token.

// POST /auth/code
public function send(Request $request)
{
    VerificationCode::create($request->validate(['email' => 'required|email'])['email']);

    return response()->noContent(); // no "user not found" leakage
}

// POST /auth/login
public function login(Request $request)
{
    $data = $request->validate([
        'email' => 'required|email',
        'code'  => 'required|string',
    ]);

    abort_unless(VerificationCode::verify($data['email'], $data['code']), 422);

    $user = User::where('email', $data['email'])->firstOrFail();
    Auth::login($user);
    $request->session()->regenerate();

    return redirect()->intended('/dashboard');
}

Gate the send endpoint harder than the verify endpoint. The package handles attempts against a code; it does not stop someone hammering POST /auth/code to spam a victim's inbox. Add ->middleware('throttle:3,1') on the send route and consider a per-identifier cooldown on top.

Override the notification, not the package. Point the config's notification class at your own VerificationCodeNotification when you need SMS/WhatsApp, translated copy, or a branded mail template. That's the intended extension seam — subclass the package notification and change only the channel.

Registration verification. Generate the code right after User::create(), then guard protected routes with a middleware that checks a verified_at column you control. The package's verifyCode() returns a boolean; it does not flip a column for you, and you probably don't want it to.

Testing. Fake the notification channel so tests don't hit the mailer, then exercise the real code path:

Notification::fake();

$this->post('/auth/code', ['email' => '[email protected]']);
$this->post('/auth/login', ['email' => '[email protected]', 'code' => '000000'])
     ->assertStatus(422);

For happy-path tests where you need the actual digits, call generateVerificationCode() directly on a model instance and use its return value, rather than trying to extract the code from a faked notification payload.

Gotchas and Tips

  • Check how codes are stored. Look at the migration before you ship. If the code lands in the table in plaintext, treat the table as a credential store: restrict DB access, and clear rows on verify. If you need hashing and it isn't there, that's a fork-or-wrap decision, not a config toggle.
  • Single-use behaviour is the thing to verify first. Write a test asserting that a code cannot be replayed after success and that an expired code fails, before you build anything on top. These are the two properties every reviewer will ask about.
  • Verify the expiry clock matches your expectations. TTL is config-driven; if your queue is backed up, a code generated at t=0 may arrive at t=45 and be nearly dead. For slow channels (SMS aggregators), shorten the queue path or lengthen the TTL deliberately.
  • Identifier normalisation. Emails are matched as strings. Lowercase and trim before both create() and verify(), or [email protected] and [email protected] become two different codes and your users lose one of them.
  • Enumeration via timing and status codes. verify() returning false is fine, but a create() that 404s on unknown users hands out a user list. Always return 204 from the send endpoint.
  • Not TOTP. This is one-time codes over a notification channel, not authenticator-app TOTP and not WebAuthn. If a security questionnaire asks for RFC 6238, this package is the wrong tool — reach for pragmarx/google2fa alongside it rather than stretching this one.
  • Small-maintainer risk. 58 stars, zero dependents, infrequent releases. Pin the version in composer.json, and keep the integration behind a thin service class (CodeService::send() / CodeService::consume()) so swapping the underlying package is a one-file change. The trait is convenient, but the trait is also the hardest thing to rip out later — consider the facade even when a model is available.
  • Publish and commit the config and migration. Don't rely on package defaults surviving a version bump, and don't let vendor:publish overwrite a migration that has already run in production.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource