0

Password reset flow — tokens, notifications, expiry

Intermediate5 min read·lv-16-006
security

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:

  1. User registers → Registered event fires.
  2. SendEmailVerificationNotification listener (auto-registered if model implements MustVerifyEmail) sends VerifyEmail notification.
  3. Notification email contains a signed URL.
  4. User clicks link → VerifyEmailController validates the signed URL and calls markEmailAsVerified().

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
<?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