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.
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.
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.
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.create() and verify(), or [email protected] and [email protected] become two different codes and your users lose one of them.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.pragmarx/google2fa alongside it rather than stretching this one.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.vendor:publish overwrite a migration that has already run in production.How can I help you explore Laravel packages today?