Password reset flow — tokens, notifications, expiry
Concept
Email verification ensures users own the email address they registered with. Laravel provides the MustVerifyEmail contract and built-in verification notification/routes.
MustVerifyEmail contract: Add use Illuminate\Contracts\Auth\MustVerifyEmail and implements MustVerifyEmail to the User model. Provides: hasVerifiedEmail(), markEmailAsVerified(), sendEmailVerificationNotification().
Required database column: email_verified_at timestamp, nullable (null = not verified). Created by default in the users migration.
Verification flow:
- User registers →
Registeredevent fires. SendEmailVerificationNotificationlistener (auto-registered if model implementsMustVerifyEmail) sendsVerifyEmailnotification.- Notification email contains a signed URL.
- User clicks link →
VerifyEmailControllervalidates the signed URL and callsmarkEmailAsVerified().
Signed URLs: Laravel signed URLs ensure the verification link hasn't been tampered with. URL::signedRoute('verification.verify', [...]). They include a hash of the URL parameters. The signed middleware validates this hash.
verified middleware: Route::middleware(['auth', 'verified']). Redirects unverified users to the verification notice page (/email/verify). Use this to protect routes that require verified emails.
Resending verification: POST /email/verification-notification endpoint triggers another verification email. Rate-limited with the throttle:6,1 middleware.
Code Example
<?php
// User model
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements MustVerifyEmail
{
// $hasVerifiedEmail(), $markEmailAsVerified(), $sendEmailVerificationNotification()
// — all provided by the MustVerifyEmail contract via the Illuminate\Auth\MustVerifyEmail trait
}
// Routes — add to routes/web.php (Auth::routes handles this with Fortify/Breeze)
Route::get('/email/verify', [EmailVerificationPromptController::class, '__invoke'])
->middleware('auth')
->name('verification.notice');
Route::get('/email/verify/{id}/{hash}', [VerifyEmailController::class, '__invoke'])
->middleware(['auth', 'signed', 'throttle:6,1'])
->name('verification.verify');
Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
->middleware(['auth', 'throttle:6,1'])
->name('verification.send');
// Protecting routes with 'verified' middleware
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', DashboardController::class);
Route::get('/orders', [OrderController::class, 'index']);
});
// Manually verify in tests / seeders
$user->markEmailAsVerified();
// Manually send verification
$user->sendEmailVerificationNotification();
// Custom verification email
class User extends Authenticatable implements MustVerifyEmail
{
public function sendEmailVerificationNotification(): void
{
$this->notify(new \App\Notifications\CustomVerifyEmailNotification());
}
}
// Check verification status
auth()->user()->hasVerifiedEmail(); // true/false
auth()->user()->email_verified_at; // Carbon or null