Реализация авторизации через 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());
}
}
Signed URL
Laravel генерирует подписанный URL с истечением через 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 дней.







