Питання на співбесіді: Автентифікація
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
10 питань
Верифікація email підтверджує, що користувач справді володіє вказаною адресою. Laravel має це з коробки.
// 1. Модель реалізує контракт
class User extends Authenticatable implements MustVerifyEmail {}
// 2. Маршрути захищають middleware
Route::get('/dashboard', ...)->middleware(['auth', 'verified']);
Як працює: після реєстрації Laravel шле лист із підписаним URL (signed URL), перехід за яким ставить email_verified_at. Middleware verified не пускає непідтверджених користувачів. Подія Registered автоматично тригерить відправку листа.
Auth::attempt() перевіряє облікові дані й, якщо вони правильні, відкриває сесію.
public function login(LoginRequest $request): RedirectResponse
{
if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
throw ValidationException::withMessages(['email' => __('auth.failed')]);
}
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
Покроково:
- Провайдер гарда шукає користувача за всіма полями, крім пароля.
- Пароль порівнюється з хешем через
Hash::check()- відкритий пароль ніде не зберігається й не порівнюється напряму. - При успіху ID користувача записується в сесію, і наступні запити бачать його через
auth()->user().
Що робиться після входу:
session()->regenerate()- новий ідентифікатор сесії, щоб підкинутий заздалегідь не дав зловмиснику увійти разом з жертвою (session fixation).redirect()->intended()- повертає туди, куди користувач ішов до перенаправлення на логін.
Додаткові умови передають тим самим масивом: Auth::attempt([...$credentials, 'is_active' => true]) не пустить вимкнений акаунт. У стартових наборах Laravel 13 усе це вже зроблено пакетом Fortify.
Звичайна сесія закінчується через SESSION_LIFETIME хвилин бездіяльності. «Запам'ятати мене» дозволяє лишатися в системі довше - без повторного входу.
Auth::attempt($credentials, remember: true);
Як це влаштовано: Laravel генерує випадковий токен, зберігає його в колонці users.remember_token і ставить довгоживучу зашифровану cookie. Коли сесія вже закінчилась, а cookie є, користувача автентифікують за нею, і Auth::viaRemember() повертає true.
Ризики й що з ними роблять:
- Вкрадена cookie = доступ на весь термін її дії. Тому cookie має
HttpOnlyіSecure(через HTTPS), а на чутливих діях пароль запитують ще раз - middlewarepassword.confirm. - Спільний комп'ютер. Галочка на бібліотечному ПК лишить акаунт відкритим для наступного. Тому вона не має бути ввімкнена за замовчуванням.
- Вихід скидає токен.
Auth::logout()оновлюєremember_token, і стара cookie перестає працювати - тож «вийти» справді виходить.
Якщо viaRemember() повертає true, розумно обмежити найнебезпечніші дії, доки людина не підтвердить пароль.
Це різні рівні автентифікації:
- Breeze - стартовий набір UI: реєстрація/вхід/скидання пароля на Blade+Livewire або React/Vue. Для швидкого старту.
- Fortify - headless-бекенд автентифікації (без UI): логіка реєстрації, 2FA, скидання пароля. Під ним працює Breeze/Jetstream.
- Sanctum - легка автентифікація для SPA (через cookie) та простих API-токенів. Дефолт для більшості API.
- Passport - повноцінний OAuth2-сервер: видача access/refresh токенів стороннім клієнтам. Обирають, коли потрібен саме OAuth2.
Правило: SPA чи мобільний застосунок → Sanctum; «увійти через наш сервіс» для третіх сторін → Passport.
Sanctum дає два режими:
1. API-токени - модель випускає токен, який клієнт шле в заголовку Authorization: Bearer ...:
$token = $user->createToken('mobile', ['posts:read'])->plainTextToken;
Route::middleware('auth:sanctum')->get('/user', fn (Request $r) => $r->user());
2. SPA-автентифікація - для односторінкових застосунків на тому ж домені використовує звичайні сесійні cookie + CSRF (без зберігання токенів у JS, що безпечніше).
Токени підтримують abilities (scopes): $user->tokenCan('posts:read'). Трейт HasApiTokens додає tokens()-зв'язок і createToken().
Автентифікація в Laravel тримається на двох поняттях з config/auth.php.
- Гард визначає, як користувача впізнають у запиті:
session- за сесією й cookie,sanctum- ще й за токеном у заголовку. - Провайдер визначає, звідки користувача беруть: зазвичай Eloquent-модель
Userчи таблиця бази.
'guards' => [
'web' => ['driver' => 'session', 'provider' => 'users'],
'admin' => ['driver' => 'session', 'provider' => 'admins'],
],
'providers' => [
'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class],
'admins' => ['driver' => 'eloquent', 'model' => App\Models\Admin::class],
],
Коли другий гард виправданий: коли це справді інші люди в іншій таблиці - адміністратори окремо від клієнтів, з окремим входом і сесією.
Route::middleware('auth:admin')->group(fn () => /* ... */);
Auth::guard('admin')->attempt($credentials);
Коли не потрібен: якщо різниця лише в правах. «Адмін - теж користувач, але з роллю» - це авторизація (ролі, політики), а не другий гард. Зайвий гард подвоює логіку входу, скидання пароля й верифікації пошти.
Обмежити кількість невдалих спроб - за ключем, що поєднує email та IP.
public function authenticate(): void
{
$key = Str::transliterate(Str::lower($this->input('email'))) . '|' . $this->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
throw ValidationException::withMessages([
'email' => __('auth.throttle', ['seconds' => RateLimiter::availableIn($key)]),
]);
}
if (! Auth::attempt($this->only('email', 'password'))) {
RateLimiter::hit($key);
throw ValidationException::withMessages(['email' => __('auth.failed')]);
}
RateLimiter::clear($key);
}
Чому саме такий ключ:
- Лише IP - заблокує цілий офіс за NAT, а зловмисник із сотнею адрес обійде.
- Лише email - будь-хто зможе заблокувати вхід чужому акаунту, просто вводячи неправильний пароль.
- Email + IP гальмує перебір конкретного акаунта з конкретної адреси.
Що ще допомагає:
- однакове повідомлення «Невірний email або пароль» - щоб не підказувати, які email зареєстровані;
- не блокувати акаунт назавжди після N спроб - це інструмент для атаки на доступність;
- двофакторна автентифікація, після якої перебір пароля сам по собі нічого не дає.
Стартові набори Laravel 13 вже обмежують спроби входу. Якщо застосунок за проксі (Cloudflare), перевірте довірені проксі - інакше всі запити матимуть IP проксі.
2FA вимагає двох факторів: «що знаю» (пароль) + «що маю» (код із застосунку/SMS). Найпоширеніше - TOTP (Time-based One-Time Password) сумісно з Google Authenticator.
У Laravel найпростіше через Fortify, який має 2FA з коробки:
- генерація секрету й QR-коду для прив'язки;
- перевірка 6-значного коду при вході;
- одноразові recovery codes на випадок втрати пристрою.
// Fortify вмикає features:
Features::twoFactorAuthentication(['confirm' => true]),
Під капотом - пакет pragmarx/google2fa. Важливо: зберігати секрет зашифрованим, давати recovery-коди, за бажанням «запам'ятати пристрій».
Докладніше в документації: Двофакторна автентифікація (Fortify)
Типовий випадок - користувач змінив пароль, бо підозрює злам, і стара сесія на чужому пристрої має перестати працювати.
Для сесій у браузері:
Route::middleware(['auth', 'auth.session'])->group(function () {
// ...
});
Auth::logoutOtherDevices($request->input('current_password'));
logoutOtherDevices() перехешовує пароль, а middleware auth.session (AuthenticateSession) на кожному запиті порівнює хеш, збережений у сесії, з поточним. На інших пристроях хеші більше не збігаються - сесія закривається. Без auth.session на маршрутах виклик нічого не дає, і це найчастіша помилка.
Для API-токенів Sanctum механізм інший - токени відкликають явно:
$user->tokens()->delete(); // усі
$user->tokens()->where('id', $tokenId)->delete(); // один пристрій
«Запам'ятати мене» скидається через новий remember_token.
Повна картина для користувача - сторінка «Активні сесії» з пристроями й можливістю закрити окрему. Для цього сесії мають бути в базі (драйвер database), щоб їх можна було перелічити й видалити за user_id.
Після зміни пароля варто також надіслати лист «Ваш пароль змінено» - якщо це зробив не власник, він дізнається одразу.
Докладніше в документації: Скасування сесій на інших пристроях
Паролі зберігають лише як повільний односторонній хеш - bcrypt чи argon2id через Hash::make().
- Не шифрування: зашифроване відновлюється ключем, і витік ключа відкриває всі паролі.
- Не SHA-256 чи MD5: вони швидкі, і на відеокарті перебирають мільярди варіантів за секунду.
- Сіль генерується для кожного пароля окремо й зберігається в самому рядку хешу - окремо нічого не треба.
Посилення без скидання. Параметри хешування з часом піднімають - наприклад, BCRYPT_ROUNDS з 12 до 13. Старі хеші при цьому лишаються робочими, бо параметри записані в них самих.
Оновити їх можна лише в момент, коли відомий відкритий пароль, - при вході:
if (Hash::needsRehash($user->password)) {
$user->update(['password' => Hash::make($plainPassword)]);
}
У Laravel це робиться автоматично: опція rehash_on_login у config/hashing.php увімкнена за замовчуванням, і після успішного входу хеш перезаписується з новими параметрами.
Перехід на інший алгоритм (bcrypt → argon2id) потребує ще одного кроку. За замовчуванням Hash::check() відхиляє хеш, створений іншим алгоритмом, і кидає RuntimeException - це захист від підміни хешу. На час переходу ставлять HASH_VERIFY=false, і тоді перезапис при вході поступово переведе активних користувачів. Неактивні лишаться на старому, і для них можна запланувати примусове скидання через рік-два, а потім повернути перевірку.
Докладніше в документації: Визначення потреби в перехешуванні