jrean/laravel-user-verification
Adds email verification to Laravel out of the box: a trait and migration for a verified flag, token-based verification emails, routes and middleware to protect verified-only areas, plus configurable events and redirects. Easy to drop into existing user models.
Check your Laravel version before you run composer require. As of v14.0.0 this package targets Laravel 13 only. The release notes are a single line ("Laravel 13"), which almost certainly means a bumped illuminate/* constraint rather than new features — so pin the major version that matches your framework instead of floating:
# Laravel 13
composer require "jrean/laravel-user-verification:^14.0"
# Laravel 10/11/12 — stay on the prior major
composer require "jrean/laravel-user-verification:^13.0"
Then publish the two things you actually need:
php artisan vendor:publish --provider="Jrean\UserVerification\UserVerificationServiceProvider" --tag="config"
php artisan vendor:publish --provider="Jrean\UserVerification\UserVerificationServiceProvider" --tag="migrations"
php artisan migrate
The migration adds verified (boolean, default false) and verification_token (nullable string) to the users table. The config file (config/user-verification.php) is the control panel: user table name, token length, mail from/subject, and the redirect targets after verification success/error.
Wire the model — add the trait to your User:
use Jrean\UserVerification\Traits\UserVerification;
class User extends Authenticatable
{
use UserVerification;
protected $fillable = ['name', 'email', 'password', 'verified', 'verification_token'];
protected $hidden = ['password', 'remember_token', 'verification_token'];
}
Register the middleware alias in bootstrap/app.php (Laravel 11+ style) or app/Http/Kernel.php (older). On Laravel 13 you're on the bootstrap/app.php path:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'is_verified' => \Jrean\UserVerification\Middleware\IsVerified::class,
]);
})
Look first at three places: config/user-verification.php (what you can change without touching code), Jrean\UserVerification\Traits\UserVerification (the methods available on the model), and php artisan route:list | grep email-verification (the routes the package registered). The package has historically registered email-verification/check/{token}, email-verification/resend, and email-verification/error; some versions use verification.* names. Always confirm with route:list rather than trusting docs — especially after a major bump.
First use case: after registration, flip the user to unverified, generate a token, and send the mail in one call:
$user = User::create($validated);
UserVerification::generate($user); // token + email in one shot
return redirect()->route('email-verification.error');
The user clicks the link, the package's controller verifies the token, and they land on your configured redirect.
Registration flow. Creating the user and generating the token are two separate concerns — wrap them so a mail failure doesn't leave a half-created account, or accept the tradeoff and let the user hit "resend." UserVerification::generate($user) is the ergonomic path; $user->generateVerificationToken() then $user->sendVerificationEmail() is the decomposed version when you need to queue or delay the mail.
Resend is a route, not a method call. The package exposes an authenticated resend endpoint; protect it with auth and rate-limit it. For admin tooling or CLI backfills, call the service directly:
UserVerification::regenerate($user); // new token, old link dies
Gate the app with the middleware. The intended pattern is a "verify your email" holding page rather than blocking login:
Route::middleware(['auth', 'is_verified'])->group(function () {
Route::get('/home', ...);
});
Route::middleware(['auth'])->group(function () {
Route::get('/email-verification/error', ...);
});
Listen to events instead of editing the package. The package fires an event when verification succeeds. That is where you attach welcome emails, role assignment, onboarding state, or analytics:
Event::listen(UserVerified::class, function ($event) {
$event->user->assignRole('member');
});
Confirm the exact event class and payload in the package source — the names have shifted across majors (UserVerified, UserVerificationEmailSent, UserVerificationEmailResent). A major bump with a one-line changelog is exactly the situation where you read the source, not the release notes.
Customize the mail, not the controller. The config controls from/subject; for real branding, subclass or replace the notification class and rebind it in a service provider, or listen to the "email sent" event and dispatch your own queued notification with a custom markdown template. Do not fork the package for copy changes.
API/SPA pattern. Don't reuse the package's redirect-based routes for a JSON client. Instead:
// POST /api/email/verify/{token} — your own controller
if (! $user->verify($token)) {
return response()->json(['message' => 'Invalid token'], 422);
}
return response()->json(['verified' => true]);
And return verified as a boolean in your user resource so the frontend can render the gate. Add a small custom middleware that aborts 403 for unverified API users — the stock is_verified middleware redirects, which is wrong for JSON.
Testing. Mail::fake(), Event::fake(), and Notification::fake() around a registration test lets you assert the token was generated and the event fired without touching SMTP. For verification tests, read $user->verification_token from the DB and hit the route directly. After upgrading to v14, run the existing verification test suite first — it's the cheapest way to catch a signature change the changelog didn't mention.
Major version = re-verify the surface area. v14.0.0 ships with a one-line changelog. Before upgrading, diff src/ against the version you're on (trait methods, service provider bindings, route definitions, event class names, config keys). Publish-and-diff the config too:
php artisan vendor:publish --provider="Jrean\UserVerification\UserVerificationServiceProvider" --tag="config" --force
A silent config key rename is the classic post-upgrade "verification stopped working" cause.
Version-lock to your framework. Don't leave this package on ^13.0 if you're moving to Laravel 13, and don't upgrade to ^14.0 on Laravel 12 — Composer will usually block you, but a loose constraint plus --ignore-platform-reqs will get you a package that fails at runtime rather than install time.
Do not mix this with Laravel's native MustVerifyEmail. Laravel uses email_verified_at; this package uses a verified boolean plus verification_token. Running both means two sources of truth, two middleware, and a model that lies to at least one of them. This matters more now than it used to: on Laravel 13 the native flow is mature and framework-supported, so the honest recommendation for greenfield work is native MustVerifyEmail. Choose this package when you specifically want the opaque token-in-URL flow, you're maintaining a legacy app, or you need the regenerate()/resend semantics it provides. Pick one on day one.
Existing users become unverified. The migration defaults verified to false. If you install this on a live app, every existing account is immediately locked out behind is_verified. Add a follow-up migration or seeder that sets verified = true for accounts created before the install date.
Hide the token. verification_token is a bearer credential. Put it in $hidden on the model, never expose it in API resources or logs, and know that it's stored in plaintext in the database — anyone with a DB dump can verify any pending account.
Tokens never expire by default. That's a real weakness: a link leaked in a forwarded email works forever, and regenerate() is the only thing that kills a prior token. If you need expiry, add a verification_token_created_at column and check it in a custom verification controller, or schedule a job that nulls stale tokens.
The redirects need named routes that exist. The config points at route names for success and error. If those routes don't exist in your app (or you renamed home), verification "succeeds" and then throws a RouteNotFoundException — which looks like a verification bug but isn't. Verify every name in config/user-verification.php resolves.
Route caching and overrides. If you replace the package's routes with your own controllers, remember php artisan route:cache will happily keep serving the old set until you clear it. Also watch for collisions if you already had an email-verification/* prefix.
Queue the mail. The default send is synchronous. Registration requests will block on SMTP round-trips, and a slow mail server means a slow signup. Swap in a queued notification (implements ShouldQueue) or dispatch the send from a job.
Debugging checklist when a link "doesn't work": confirm verification_token is non-null in the DB, confirm the URL in the mail matches route('email-verification.check', $token), confirm verified is still 0, and check whether a resend silently regenerated the token after the email was sent. The most common cause is a stale token from a duplicate generate() call.
Extension points, in order of preference: events → custom notification → config → replacing the controller via your own route definitions → subclassing the service and rebinding it in the container. Forking should be last; the package is small enough that a thin wrapper is usually cheaper than a fork you have to maintain — and a fork is especially painful when the upstream majors track Laravel majors on a one-line changelog.
How can I help you explore Laravel packages today?