Реалізація Magic Link аутентифікації на веб-сайті
Magic Link — вход без пароля: користувач вводить email, отримує лист зі посиланням, клікає — і авторизований. Без паролів, без форм підтвердження. Популярна в SaaS-продуктах та інструментах для розробників (Notion, Slack, Linear).
Принцип роботи
1. POST /auth/magic-link { email: "[email protected]" }
→ генерація токену (32 байти random)
→ збереження hash(токену) у БД/Redis з TTL 15 хв
→ відправка листа зі посиланням /auth/magic-link/verify?token=...
→ відповідь: { message: "Лист відправлено" }
2. GET /auth/magic-link/verify?token=...&email=...
→ пошук токену у БД
→ верифікація: не закінчився, hash збігається
→ одноразове використання: видалити токен
→ авторизація користувача
→ редирект на /dashboard
Генерація та зберігання токену
class MagicLinkService
{
public function sendLink(string $email): void
{
$this->checkRateLimit($email);
$user = User::firstOrCreate(
['email' => $email],
['name' => explode('@', $email)[0], 'email_verified_at' => now()]
);
$token = Str::random(64);
$hashedToken = hash('sha256', $token);
// Інвалідувати попередні токени для цього користувача
MagicLinkToken::where('user_id', $user->id)->delete();
MagicLinkToken::create([
'user_id' => $user->id,
'token' => $hashedToken,
'expires_at' => now()->addMinutes(15),
]);
Mail::to($email)->send(new MagicLinkMail($user, $token));
}
public function authenticate(string $token, string $email): User
{
$hashedToken = hash('sha256', $token);
$record = MagicLinkToken::where('token', $hashedToken)
->whereHas('user', fn($q) => $q->where('email', $email))
->where('expires_at', '>', now())
->whereNull('used_at')
->firstOrFail();
// Одноразове використання
$record->update(['used_at' => now()]);
return $record->user;
}
}
Міграція
Schema::create('magic_link_tokens', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('token', 64)->unique(); // SHA256 хеш
$table->timestamp('expires_at');
$table->timestamp('used_at')->nullable();
$table->string('ip_address', 45)->nullable();
$table->string('user_agent')->nullable();
$table->timestamps();
$table->index(['token', 'expires_at']);
});
Верифікація токену
class MagicLinkController extends Controller
{
public function verify(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required|string|size:64',
'email' => 'required|email',
]);
try {
$user = $this->magicLinkService->authenticate(
$request->token,
$request->email
);
} catch (ModelNotFoundException) {
return redirect('/login?error=invalid_link');
}
Auth::login($user, remember: true);
// Підтвердити email при першому вході
if (!$user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
}
return redirect()->intended('/dashboard');
}
}
Rate limiting
RateLimiter::for('magic-link', function (Request $request) {
return [
Limit::perMinutes(5, 1)->by('email:' . $request->email), // 1 за 5 хвилин
Limit::perHour(5)->by('ip:' . $request->ip()), // 5 на годину з IP
];
});
Шаблон листа
Важливі деталі листа:
- Кнопка «Вхід» з токеном у href
- Строк дії (15 хвилин)
- Якщо не запрошували — ігнорувати
- Текстова версія з повним посиланням (для поштових клієнтів без HTML)
class MagicLinkMail extends Mailable
{
use Queueable, SerializesModels;
public string $loginUrl;
public function __construct(User $user, string $token)
{
$this->loginUrl = URL::signedRoute(
'auth.magic-link.verify',
['token' => $token, 'email' => $user->email],
now()->addMinutes(15)
);
}
public function build(): self
{
return $this->subject('Ваше посилання для входу')
->markdown('emails.magic-link');
}
}
Безпека
Кілька нюансів, які часто упускають:
- Токен у URL — логується у access_log сервера. HTTPS обов'язковий
- Один токен за раз — при запросі нового токену старі інвалідувати
- IP та User-Agent — зберігати для аудиту, але не використовувати для блокування (користувач міг відкрити лист на іншому пристрої)
- Переспівання — після кліку токен позначається як використаний, не видаляється — для виявлення повторних спроб
Тимчасовість роботи
| Етап | Час |
|---|---|
| Сервіс генерації + зберігання | 1 день |
| Верифікація + rate limiting | 0,5 дня |
| Шаблон листа + черга | 0,5 дня |
| Frontend форма + UI стани | 0,5 дня |
| Тести + edge cases | 0,5 дня |
Всього: 3–4 робочих дні.







