Впровадження аутентифікації з підтвердженням email для веб-сайтів
Аутентифікація через email з підтвердженням — схема, при якій користувач вводить адресу електронної пошти, отримує листа зі посиланням або кодом та завершує вхід за цим посиланням. Це варіант passwordless-аутентифікації з явним кроком верифікації власності.
Відрізняється від Magic Link тим, що підтвердження email може бути окремим кроком під час традиційної реєстрації — перевірка факту існування й доступності поштової скриньки.
Два сценарії використання
Сценарій 1: Верифікація при реєстрації Користувач реєструється з паролем → отримує лист → підтверджує почту → отримує доступ до функцій.
Сценарій 2: Passwordless вхід через email Користувач вводить email → отримує лист зі посиланням → клікає → авторизований. Без пароля взагалі.
Верифікація email при реєстрації (Laravel)
// Модель реалізує MustVerifyEmail
class User extends Authenticatable implements MustVerifyEmail
{
// ...
}
// routes/auth.php
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');
Користувацький шаблон листа:
class User extends Authenticatable implements MustVerifyEmail
{
public function sendEmailVerificationNotification(): void
{
$this->notify(new CustomVerifyEmailNotification());
}
}
Підписане посилання
Laravel генерує підписане посилання, яке спливає через 60 хвилин:
$url = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
['id' => $user->id, 'hash' => sha1($user->email)]
);
Підпис — HMAC-SHA256 з APP_KEY. Спроби підробки або зміни параметрів повертають 403.
OTP-код замість посилання
Деякі проекти користуються 6-значним кодом замість посилання — зручніше при відкритті листа на іншому пристрої:
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
Cache::put(
"email_verification:{$user->id}",
hash('sha256', $code),
now()->addMinutes(10)
);
// Верифікація коду
public function verify(Request $request): JsonResponse
{
$stored = Cache::get("email_verification:{$user->id}");
if (!$stored || !hash_equals($stored, hash('sha256', $request->code))) {
return response()->json(['message' => 'Неверний або застарілий код'], 422);
}
$user->markEmailAsVerified();
Cache::forget("email_verification:{$user->id}");
return response()->json(['message' => 'Email підтверджений']);
}
Повторна відправка та захист від спаму
// Кулдаун між запитами на повторну відправку
RateLimiter::for('email-verification', function (Request $request) {
return Limit::perMinutes(5, 1)->by($request->user()->id);
});
Фронтенд показує зворотний відлік до можливості повторної відправки — зазвичай 60 секунд.
Спливання та невалідні посилання
Якщо користувач клікає на спливле посилання — перенаправити на сторінку з можливістю запитати новий лист. Не показувати загальну помилку 403 без пояснень.
Зміна email
При зміні email адреса не змінюється одразу — спочатку відправляється підтвердження на нову скриньку. Тільки після підтвердження email оновлюється. Це захищає від перехоплення аккаунту через зміну пошти.
// pending_email у таблиці users або окрема таблиця email_changes
$user->update(['pending_email' => $newEmail]);
// Відправити верифікацію на $newEmail
// При підтвердженні: $user->update(['email' => $newEmail, 'pending_email' => null])
Часова розкладка робіт
| Етап | Час |
|---|---|
| Верифікація при реєстрації (стандартна) | 1 день |
| Користувацькі шаблони листів | 0.5 дня |
| OTP-код замість посилання | 1 день |
| Зміна email з підтвердженням | 1 день |
| Тести + edge cases | 1 день |
Базова верифікація при реєстрації — 1–2 дні. Повний флоу зі змінією email та OTP — 4–5 днів.







