<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
LARAVEL · MIDDLE ЧАСТО ПИТАЮТЬ

Як влаштована автентифікація в Laravel: guards, providers, Sanctum і коли потрібен Passport?

Guard відповідає на питання «як пізнати користувача в цьому запиті» (session, token, sanctum), provider - «де його взяти» (eloquent-модель або таблиця через `database`). Sanctum закриває обидва типові випадки: персональні токени в `personal_access_tokens` для мобільних і сторонніх скриптів та звичайну cookie-сесію для свого SPA на тому ж домені. Passport потрібен лише тоді, коли ви самі стаєте OAuth2-провайдером із клієнтами, екраном згоди й refresh-токенами, а готовий фронтенд для входу беруть зі стартових наборів Laravel 12 (React, Vue, Livewire), бо Breeze і Jetstream уже не той шлях, який радить документація.

Чим guard відрізняється від user provider у `config/auth.php` і що з них відповідає за таблицю користувачів?
SPA на тому ж домені, Sanctum: навіщо запит на `/sanctum/csrf-cookie` і що зламається, якщо його пропустити?
Користувач зайшов у SPA через сесію - що повернe `$request->user()->tokenCan('orders:write')`?
Нам треба «увійти через наш акаунт» для сторонніх застосунків. Беремо Sanctum чи Passport?
Чому в новому проєкті на Laravel 12 немає ні Breeze, ні Jetstream?
автентифікація guards Sanctum Passport starter kits API-токени

Уся автентифікація Laravel збирається з двох деталей, описаних у config/auth.php. Guard відповідає на питання, як пізнати користувача в цьому конкретному запиті: драйвер session читає ідентифікатор із сесії, token порівнює параметр api_token з колонкою в таблиці, sanctum і passport додають свої драйвери при встановленні пакетів. Provider заходить з іншого боку: звідки взяти користувача за цим ідентифікатором і чим перевірити пароль. Драйвер eloquent бере модель із ключа model і працює через EloquentUserProvider, драйвер database ходить у таблицю напряму. Інтерфейс UserProvider короткий (retrieveById, retrieveByToken, retrieveByCredentials, validateCredentials, updateRememberToken і доданий у Laravel 11 rehashPasswordIfRequired), тож власний provider для LDAP або зовнішнього сервісу - це один клас і Auth::provider('ldap', ...) у сервіс-провайдері. Для зовсім простих випадків є Auth::viaRequest('header-token', fn (Request $r) => ...), який робить guard з одного замикання.

Класичний веб-логін працює так: Auth::attempt(['email' => ..., 'password' => ...], $remember) просить provider знайти користувача за всіма переданими полями, крім password, перевіряє пароль через Hash::check() і за успіху кладе в сесію ключ login_web_<sha1 повного імені класу guard> зі значенням $user->getAuthIdentifier(). У самій сесії немає ні пароля, ні ролей - лише id, тому наступний запит щоразу читає користувача з БД. $remember додає cookie remember_web_<той самий хеш> із трьох частин: id, remember_token і хеш пароля. Звідси два практичні наслідки. Auth::logout() викликає cycleRememberToken(), тобто перезаписує remember_token, і всі старі remember-cookie стають непридатні; а middleware auth.session (AuthenticateSession) порівнює password_hash_web у сесії з поточним хешем пароля й вилогінює сесію, якщо пароль змінили. Без цього middleware Auth::logoutOtherDevices() не працює, і зміна пароля не вибиває зловмисника, який уже має живу сесію. Паролі ж хешуються bcrypt (або argon2id через config/hashing.php), а з Laravel 11 вмикається автоматичний rehash на логіні, коли cost у конфігу виріс.

Sanctum надбудовується над цією ж механікою й закриває два найчастіші випадки; плутанини тут найбільше. Перший режим - персональні токени: $user->createToken('Mobile', ['orders:read'], now()->addDays(30)) пише рядок у personal_access_tokens (поліморфний tokenable, json-колонка abilities, last_used_at, expires_at із Sanctum 3.2) і повертає plainTextToken виду 7|<секрет>. У базі лежить hash('sha256', $секрет), тому показати токен удруге неможливо за задумом. Другий - звичайна cookie-сесія для свого SPA: EnsureFrontendRequestsAreStateful, доданий через $middleware->statefulApi(), для запитів із домену зі списку sanctum.stateful вмикає StartSession і ValidateCsrfToken, а guard sanctum спершу питає web-guard і лише потім дивиться на Authorization: Bearer. Тому Sanctum у SPA-режимі не видає жодного токена: клієнт стукає на /sanctum/csrf-cookie, отримує XSRF-TOKEN, логіниться звичайним POST на /login і далі живе на сесійному cookie, який HttpOnly і не дістається з JavaScript. Умови режиму жорсткі: спільний top-level домен, домен у sanctum.stateful, supports_credentials => true у config/cors.php і withCredentials на клієнті. Мобільний застосунок або чужий сервер цих умов не виконує, тому їм дають токени.

Passport вирішує іншу задачу. Це реалізація OAuth2-сервера на league/oauth2-server із власними таблицями клієнтів і токенів, RSA-ключами з passport:keys, підписаними JWT-доступами, refresh-токенами, екраном згоди на /oauth/authorize і scopes, які клієнт просить у протоколі. Він потрібен, коли логінитись у вас будуть сторонні застосунки: «увійти через наш сервіс», інтеграції партнерів, кілька незалежних продуктів навколо одного акаунта, machine-to-machine доступ через client credentials. Рекомендований флоу - authorization code з PKCE; password і implicit grant у Passport 11+ вимкнені за замовчуванням і включаються окремими Passport::enablePasswordGrant() та enableImplicitGrant(), бо OAuth 2.1 від них відмовився. Якщо чужих клієнтів немає, Passport додає лише роботу: ротацію ключів, прибирання токенів, довший цикл логіну й додатковий шар, у якому легко помилитися. Буває й обернена ситуація: коли ви самі хочете логінити людей через Google або GitHub, цим займається Socialite, а не Passport чи Sanctum.

Готовий фронтенд для входу тепер беруть зі стартових наборів Laravel 12: React і Vue на Inertia 2 або Livewire з Volt, усі з Tailwind 4, Pest і сторінками реєстрації, скидання пароля, підтвердження email і налаштувань профілю. Є варіанти на WorkOS AuthKit, де сам логін, соцмережі, passkeys і SSO живуть за межами застосунку. Breeze і Jetstream далі встановлюються, але документація більше не веде до них нові проєкти, і будувати на них свіжий застосунок означає підписатися на шлях, який уже не розвивають. Fortify лишається окремим інструментом для випадку «свій фронтенд, чужі контролери»: маршрути для логіну, 2FA й підтвердження пароля без жодного шаблону. Залишилася межа, яку легко загубити: автентифікація закінчується там, де починається авторизація. Guard каже, хто прийшов, abilities токена кажуть, яким ключем, а право на конкретний об'єкт перевіряє policy. Якщо tokenCan('orders:read') стоїть замість authorize('view', $order), будь-який власник токена читає чужі замовлення, і на співбесіді це видно одразу.

// config/auth.php: guard каже «як пізнати», provider - «де взяти й чим перевірити пароль»
'guards' => [
    'web' => ['driver' => 'session', 'provider' => 'users'],
    // окрема сесія: ключ login_admin_<sha1 класу> не конфліктує з web
    'admin' => ['driver' => 'session', 'provider' => 'admins'],
    // драйвер реєструє сам Sanctum: спершу web-сесія, потім Bearer-токен
    'api' => ['driver' => 'sanctum', 'provider' => 'users'],
],
'providers' => [
    'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class],
    'admins' => ['driver' => 'eloquent', 'model' => App\Models\Admin::class],
],

final class DeviceTokenController
{
    public function store(Request $request): JsonResponse
    {
        $data = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required', 'string'],
            'device_name' => ['required', 'string', 'max:100'],
        ]);

        $user = User::where('email', $data['email'])->first();

        // Hash::check читає алгоритм і cost із самого хеша; порівнювати рядки не можна
        if (! $user || ! Hash::check($data['password'], $user->password)) {
            throw ValidationException::withMessages(['email' => __('auth.failed')]);
        }

        // plainTextToken існує лише тут: у БД лежить sha256 від секрету
        $token = $user->createToken(
            $data['device_name'],
            ['orders:read'],     // перевіряється через tokenCan() і middleware abilities
            now()->addDays(30),  // перебиває config('sanctum.expiration')
        );

        return response()->json(['token' => $token->plainTextToken], 201);
    }
}

// Ability звужує права токена, policy всередині вирішує доступ до конкретного замовлення
Route::middleware(['auth:sanctum', 'abilities:orders:read'])
    ->get('/orders/{order}', [OrderController::class, 'show']);
Що guard = драйвер + provider: драйвер (`session`, `token`, `sanctum`, `passport`) вирішує, звідки взяти ідентифікатор у запиті, а provider (`eloquent` з `model` або `database` з `table`) - як завантажити користувача й перевірити пароль.
Що `SessionGuard` тримає в сесії лише `login_web_<хеш класу>` з ідентифікатором, а remember-me живе в окремому cookie `remember_web_<хеш>` з трьох частин (id, `remember_token`, хеш пароля), тому `cycleRememberToken()` і зміна пароля разом із middleware `auth.session` вибивають старі сесії.
Що `auth:sanctum` у stateful-режимі - це та сама сесія: `EnsureFrontendRequestsAreStateful` для домену зі списку `sanctum.stateful` вмикає `StartSession` і перевірку CSRF, і лише для решти запитів guard дивиться на Bearer-токен.
Що в `personal_access_tokens` лежить sha256-хеш, plaintext (`ID|секрет`) показується один раз у `plainTextToken`, а abilities - це не авторизація: `tokenCan()` звужує права вже автентифікованого користувача, об'єкт усе одно перевіряє policy.
Що Passport - це сервер OAuth2 (league/oauth2-server, RSA-ключі з `passport:keys`, JWT-доступ, refresh-токени, `clients`, scopes), і брати його для власного SPA чи мобільного застосунку означає платити складністю за нічого.
Що з Laravel 12 офіційні стартові набори - React, Vue й Livewire (Inertia 2, Tailwind 4, Pest), плюс варіанти на WorkOS AuthKit; Breeze і Jetstream лишилися як спадок, а Fortify дає ті самі маршрути без жодних в'ю.
Казати «Sanctum - це для API, сесії - для веба» і не знати, що Sanctum у SPA-режимі автентифікує саме сесійним cookie через guard `web`.
Забути `withCredentials: true` у клієнті й `supports_credentials => true` у `config/cors.php`, а потім шукати причину 401 у коді guard.
Тримати SPA на `app.example.com`, API на `api.other.com` і дивуватися, що cookie не ходить: stateful-режим вимагає спільного top-level домену і домену у списку `sanctum.stateful`.
Вважати, що `abilities`/`ability` middleware працюють з коробки: їх треба самому додати в `withMiddleware(fn ($m) => $m->alias([...]))`, інакше маршрут падає з `Target class [abilities] does not exist`.
Перевіряти `tokenCan()` у коді, який обслуговує і SPA: при сесійній автентифікації користувач має `TransientToken`, і `tokenCan()` завжди `true`.
Ставити Passport, щоб «видавати токени мобільному застосунку», і тягнути password grant, який у Passport 11+ вимкнений за замовчуванням і включається лише `Passport::enablePasswordGrant()`.
Зберігати Sanctum-токен у `localStorage` для свого ж SPA на тому ж домені: будь-який XSS забирає токен, який не має ні `HttpOnly`, ні автоматичного терміну життя.
Не прибирати протерміновані токени: без `sanctum:prune-expired --hours=24` у планувальнику таблиця живе вічно, а `expires_at` на доступ не впливає - прострочений токен просто перестає автентифікувати.
ПОРАДА

Відповідайте через три питання, на які відповідає кожен шар: guard - «як пізнати користувача в цьому запиті», provider - «звідки його взяти», Sanctum - «токен чи сесія для мого ж клієнта». І додайте межу для Passport одним реченням: Passport потрібен, коли логінитись у вас будуть чужі застосунки, тобто коли вам потрібен екран згоди, `client_id` і refresh-токени. Якщо чужих клієнтів немає, OAuth2 - це сервер, який ви будете супроводжувати задарма.

оновлено 3 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

`EnsureFrontendRequestsAreStateful` для запитів із домену зі списку `sanctum.stateful` вмикає сесію і CSRF, а guard `sanctum` спершу питає `web`-guard: якщо користувач там є, йому підставляється `TransientToken`, чий `can()` погоджується з будь-якою ability. Bearer-токен перевіряється лише для решти запитів, і це звичайний запит до БД: частина до `|` - це id рядка, друга частина порівнюється як `hash('sha256', ...)` з колонкою `token`, тому plaintext ніде не лежить і другий раз його не покажеш. JWT і RSA-ключі - це Passport, Sanctum таких токенів не видає.