Verificació d'email

Com implementar verificació d'adreces de correu electrònic i restabliment de contrasenyes a Laravel.

Verificació d'email#

La verificació d'email confirma que l'adreça de correu que un usuari ha proporcionat durant el registre realment li pertany. Laravel inclou tot el necessari per enviar correus de verificació i gestionar el flux complet: enviar el correu, verificar el link, reenviar el correu si cal i protegir rutes perquè només hi puguin accedir usuaris verificats.

Preparació del model#

El primer pas és fer que el model User implementi la interfície MustVerifyEmail. Aquesta interfície indica a Laravel que ha d'enviar un correu de verificació quan es crea un nou usuari:

use Illuminate\Contracts\Auth\MustVerifyEmail;
 
class User extends Authenticatable implements MustVerifyEmail
{
    use HasFactory, Notifiable;
 
    // ...
}

La taula users ha de tenir una columna email_verified_at de tipus timestamp nullable. La migració per defecte de Laravel ja la inclou:

$table->timestamp('email_verified_at')->nullable();

Quan un usuari es registra, Laravel envia automàticament un correu amb un link de verificació signat. L'usuari fa clic al link i la columna email_verified_at s'actualitza amb la data i hora de la verificació.

Rutes de verificació#

Si utilitzes Breeze o Jetstream, les rutes de verificació ja estan configurades. Si configures l'autenticació manualment, necessites tres rutes: una per mostrar l'avís de verificació, una per verificar el link i una per reenviar el correu.

Avís de verificació#

Aquesta pàgina informa l'usuari que ha de verificar el seu correu electrònic:

Route::get('/email/verify', function () {
    return view('auth.verify-email');
})->middleware('auth')->name('verification.notice');

El nom verification.notice és important perquè el middleware verified redirigeix automàticament a aquesta ruta quan un usuari no verificat intenta accedir a una ruta protegida.

Aquesta ruta processa el link que l'usuari rep per correu:

use Illuminate\Foundation\Auth\EmailVerificationRequest;
 
Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
    $request->fulfill();
 
    return redirect('/dashboard');
})->middleware(['auth', 'signed'])->name('verification.verify');

El EmailVerificationRequest és un Form Request especial que valida automàticament que l'ID i el hash del link corresponen a l'usuari autenticat. El middleware signed verifica que la URL no ha estat manipulada.

El mètode fulfill() crida el mètode markEmailAsVerified() de l'usuari i dispara l'event Verified.

Reenviar el correu de verificació#

Si l'usuari no ha rebut el correu o ha caducat, necessita poder reenviar-lo:

use Illuminate\Http\Request;
 
Route::post('/email/verification-notification', function (Request $request) {
    $request->user()->sendEmailVerificationNotification();
 
    return back()->with('message', 'Hem enviat un nou correu de verificació.');
})->middleware(['auth', 'throttle:6,1'])->name('verification.send');

El middleware throttle:6,1 limita a 6 reenviaments per minut per evitar abusos.

Protegir rutes amb verificació#

El middleware verified impedeix l'accés a rutes si l'usuari no ha verificat el seu email:

Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::resource('articles', ArticleController::class);
    Route::resource('comments', CommentController::class);
});

Quan un usuari no verificat intenta accedir a una d'aquestes rutes, Laravel el redirigeix a la ruta verification.notice.

Personalitzar el correu de verificació#

Per defecte, Laravel utilitza la notificació Illuminate\Auth\Notifications\VerifyEmail per enviar el correu. Pots personalitzar-la de dues maneres.

Personalitzar el contingut#

El mètode més senzill és modificar el renderitzat de la notificació al AppServiceProvider:

use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;
 
public function boot(): void
{
    VerifyEmail::toMailUsing(function (object $notifiable, string $url) {
        return (new MailMessage)
            ->subject('Verifica el teu correu electrònic')
            ->greeting('Hola ' . $notifiable->name . '!')
            ->line('Gràcies per registrar-te. Fes clic al botó per verificar el teu correu.')
            ->action('Verificar correu', $url)
            ->line('Si no has creat cap compte, no cal que facis res.')
            ->salutation('L\'equip de ' . config('app.name'));
    });
}

Crear una notificació personalitzada#

Per a un control total, crea la teva pròpia notificació i sobreescriu el mètode sendEmailVerificationNotification del model User:

// Crear la notificació
php artisan make:notification CustomVerifyEmail
// app/Notifications/CustomVerifyEmail.php
namespace App\Notifications;
 
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;
 
class CustomVerifyEmail extends VerifyEmail
{
    protected function buildMailMessage($url): MailMessage
    {
        return (new MailMessage)
            ->subject('Confirma el teu compte')
            ->markdown('emails.verify', [
                'url' => $url,
                'user' => $this->id,
            ]);
    }
}
// app/Models/User.php
public function sendEmailVerificationNotification(): void
{
    $this->notify(new \App\Notifications\CustomVerifyEmail);
}

Ara pots crear una vista Markdown personalitzada a resources/views/emails/verify.blade.php amb el disseny que vulguis.

Restabliment de contrasenya#

El restabliment de contrasenya permet als usuaris recuperar l'accés al seu compte quan han oblidat la contrasenya. Laravel gestiona tot el flux: enviar un link de restabliment per correu, verificar el token i permetre que l'usuari estableixi una nova contrasenya.

Preparació#

La taula password_reset_tokens emmagatzema els tokens de restabliment. La migració per defecte de Laravel ja la inclou:

Schema::create('password_reset_tokens', function (Blueprint $table) {
    $table->string('email')->primary();
    $table->string('token');
    $table->timestamp('created_at')->nullable();
});

El primer pas del flux és un formulari on l'usuari introdueix el seu correu electrònic:

use Illuminate\Support\Facades\Password;
 
Route::get('/forgot-password', function () {
    return view('auth.forgot-password');
})->middleware('guest')->name('password.request');
 
Route::post('/forgot-password', function (Request $request) {
    $request->validate(['email' => 'required|email']);
 
    $status = Password::sendResetLink(
        $request->only('email')
    );
 
    return $status === Password::RESET_LINK_SENT
        ? back()->with('status', __($status))
        : back()->withErrors(['email' => __($status)]);
})->middleware('guest')->name('password.email');

El mètode Password::sendResetLink busca l'usuari pel correu, genera un token i envia un correu amb el link de restabliment. Si el correu no existeix, retorna un error. Si ja s'ha enviat un link recentment (dins del temps de throttle configurable), també retorna un error.

Formulari de nova contrasenya#

Quan l'usuari fa clic al link del correu, arriba a un formulari on pot introduir la nova contrasenya:

Route::get('/reset-password/{token}', function (string $token) {
    return view('auth.reset-password', ['token' => $token]);
})->middleware('guest')->name('password.reset');

Processar el restabliment#

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
 
Route::post('/reset-password', function (Request $request) {
    $request->validate([
        'token' => 'required',
        'email' => 'required|email',
        'password' => 'required|min:8|confirmed',
    ]);
 
    $status = Password::reset(
        $request->only('email', 'password', 'password_confirmation', 'token'),
        function (User $user, string $password) {
            $user->forceFill([
                'password' => Hash::make($password),
            ])->setRememberToken(Str::random(60));
 
            $user->save();
 
            event(new \Illuminate\Auth\Events\PasswordReset($user));
        }
    );
 
    return $status === Password::PASSWORD_RESET
        ? redirect()->route('login')->with('status', __($status))
        : back()->withErrors(['email' => [__($status)]]);
})->middleware('guest')->name('password.update');

El mètode Password::reset verifica el token, comprova que no ha caducat i executa la closure que restableix la contrasenya. El setRememberToken genera un nou token de "remember me" per invalidar qualsevol sessió persistent anterior. L'event PasswordReset permet executar accions addicionals, com tancar totes les sessions actives.

Personalitzar el correu de restabliment#

De la mateixa manera que amb la verificació d'email, pots personalitzar el correu de restabliment:

use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Notifications\Messages\MailMessage;
 
public function boot(): void
{
    ResetPassword::toMailUsing(function (object $notifiable, string $token) {
        $url = url(route('password.reset', [
            'token' => $token,
            'email' => $notifiable->getEmailForPasswordReset(),
        ], false));
 
        return (new MailMessage)
            ->subject('Restableix la teva contrasenya')
            ->greeting('Hola ' . $notifiable->name . '!')
            ->line('Hem rebut una sol·licitud per restablir la contrasenya del teu compte.')
            ->action('Restablir contrasenya', $url)
            ->line('Aquest link expira en ' . config('auth.passwords.users.expire') . ' minuts.')
            ->line('Si no has sol·licitat un canvi de contrasenya, no cal que facis res.');
    });
}

Configuració#

Tant la verificació d'email com el restabliment de contrasenya es configuren al fitxer config/auth.php:

// config/auth.php
'passwords' => [
    'users' => [
        'provider' => 'users',
        'table' => 'password_reset_tokens',
        'expire' => 60,    // El link caduca en 60 minuts
        'throttle' => 60,  // Esperar 60 segons entre reenviaments
    ],
],
 
'password_timeout' => 10800, // 3 hores per a confirmació de contrasenya

El camp expire controla quants minuts és vàlid el link de restabliment. El camp throttle controla quants segons ha d'esperar l'usuari entre sol·licituds de reenviament. El password_timeout afecta el middleware password.confirm.

Verificar canvis de correu#

Quan un usuari canvia la seva adreça de correu electrònic, és bona pràctica tornar a verificar-la. El mètode forceFill permet actualitzar l'email i invalidar la verificació:

public function updateEmail(Request $request)
{
    $request->validate([
        'email' => ['required', 'email', 'unique:users,email,' . $request->user()->id],
    ]);
 
    $request->user()->forceFill([
        'email' => $request->email,
        'email_verified_at' => null,
    ])->save();
 
    $request->user()->sendEmailVerificationNotification();
 
    return back()->with('status', 'Hem enviat un correu de verificació a la nova adreça.');
}

Això posa email_verified_at a null, cosa que fa que el middleware verified torni a bloquejar l'accés fins que l'usuari verifiqui la nova adreça.