HardТеория8 min

Аутентификация (Authentication)

Guards, providers, Auth facade, login/logout, запоминание пользователей, HTTP Basic Auth, кастомные guards, Sanctum vs Passport

Аутентификация в Laravel -- это гибкая система, построенная на двух ключевых концепциях: guards (определяют, КАК пользователь аутентифицирован) и providers (определяют, ОТКУДА извлекается пользователь). Конфигурация находится в config/auth.php.

Конфигурация

// config/auth.php
return [
    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'sanctum', // or 'token'
            'provider' => 'users',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        // Database provider (without Eloquent)
        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_reset_tokens',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

Guards и Providers

Guard -- КАК аутентифицировать

Session guard -- хранит состояние в сессии (для веб-приложений).

Token guard -- проверяет токен в запросе (для API).

Sanctum guard -- расширенная токенная аутентификация.

Provider -- ОТКУДА брать пользователя

Eloquent provider -- использует модель Eloquent.

Database provider -- использует прямые SQL-запросы.

// Using Auth facade with specific guard
use Illuminate\Support\Facades\Auth;

// Default guard
$user = Auth::user();

// Specific guard
$user = Auth::guard('api')->user();

// Check if user is authenticated
if (Auth::check()) {
    // User is logged in
}

// Get user ID
$id = Auth::id();

Login и Logout

Ручная аутентификация

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

final class LoginController extends Controller
{
    public function login(Request $request): RedirectResponse
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        // attempt() hashes the password and checks against the database
        if (Auth::attempt($credentials)) {
            // Regenerate session to prevent session fixation
            $request->session()->regenerate();

            return redirect()->intended('dashboard');
        }

        return back()->withErrors([
            'email' => 'Предоставленные учётные данные не соответствуют нашим записям.',
        ])->onlyInput('email');
    }

    public function logout(Request $request): RedirectResponse
    {
        Auth::logout();

        // Invalidate session
        $request->session()->invalidate();

        // Regenerate CSRF token
        $request->session()->regenerateToken();

        return redirect('/');
    }
}

Аутентификация с дополнительными условиями

// Additional conditions beyond email/password
if (Auth::attempt([
    'email' => $email,
    'password' => $password,
    'is_active' => true,  // Additional WHERE condition
])) {
    // Only active users can log in
}

// Using a closure for complex conditions
if (Auth::attempt([
    'email' => $email,
    'password' => $password,
    fn (Builder $query) => $query->whereNotNull('email_verified_at'),
])) {
    // Only verified users
}

Аутентификация конкретного экземпляра пользователя

use App\Models\User;

$user = User::find(1);

// Login a specific user instance
Auth::login($user);

// Login and "remember" the user
Auth::login($user, remember: true);

// Login by user ID
Auth::loginUsingId(1);
Auth::loginUsingId(1, remember: true);

// One-time authentication (no session/cookie, for single request)
Auth::once(['email' => $email, 'password' => $password]);

Запоминание пользователей (Remember Me)

// With remember me checkbox
if (Auth::attempt($credentials, $request->boolean('remember'))) {
    // User will be remembered
}

// Check if user was authenticated via "remember me" cookie
if (Auth::viaRemember()) {
    // User was authenticated using remember token
}

Для работы "remember me" таблица users должна содержать колонку remember_token (varchar(100), nullable).

HTTP Basic Authentication

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Illuminate\Support\Facades\Auth;

final class AuthenticateWithBasicAuth
{
    public function handle($request, $next)
    {
        // Default: authenticate using 'email' field
        return Auth::onceBasic() ?: $next($request);
    }
}

// Or use built-in middleware
// Route::middleware('auth.basic')->group(function () { ... });

Stateless HTTP Basic Auth

// For API routes - no session cookie
Route::middleware('auth.basic.once')->group(function () {
    // These routes use stateless HTTP Basic Auth
});

Кастомные Guards

<?php

declare(strict_types=1);

namespace App\Guards;

use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;

final class ApiKeyGuard implements Guard
{
    private ?Authenticatable $user = null;

    public function __construct(
        private readonly Request $request,
    ) {}

    public function check(): bool
    {
        return $this->user() !== null;
    }

    public function guest(): bool
    {
        return ! $this->check();
    }

    public function user(): ?Authenticatable
    {
        if ($this->user !== null) {
            return $this->user;
        }

        $apiKey = $this->request->header('X-API-Key');

        if ($apiKey) {
            $this->user = User::where('api_key', hash('sha256', $apiKey))
                ->first();
        }

        return $this->user;
    }

    public function id(): int|string|null
    {
        return $this->user()?->getAuthIdentifier();
    }

    public function validate(array $credentials = []): bool
    {
        $user = User::where('api_key', hash('sha256', $credentials['api_key'] ?? ''))
            ->first();

        return $user !== null;
    }

    public function hasUser(): bool
    {
        return $this->user !== null;
    }

    public function setUser(Authenticatable $user): static
    {
        $this->user = $user;

        return $this;
    }
}

Регистрация кастомного Guard

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Guards\ApiKeyGuard;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;

final class AuthServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Auth::extend('api-key', function (Application $app, string $name, array $config) {
            return new ApiKeyGuard(
                $app['request'],
            );
        });
    }
}
// config/auth.php
'guards' => [
    'api-key' => [
        'driver' => 'api-key',
        'provider' => 'users',
    ],
],

Кастомные User Providers

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;

final class LdapUserProvider implements UserProvider
{
    public function retrieveById(mixed $identifier): ?Authenticatable
    {
        // Retrieve user from LDAP by ID
    }

    public function retrieveByToken(mixed $identifier, string $token): ?Authenticatable
    {
        // Retrieve user by remember token
    }

    public function updateRememberToken(Authenticatable $user, string $token): void
    {
        // Update remember token in storage
    }

    public function retrieveByCredentials(array $credentials): ?Authenticatable
    {
        // Retrieve user by credentials (e.g., email)
    }

    public function validateCredentials(Authenticatable $user, array $credentials): bool
    {
        // Validate password against LDAP
    }

    public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void
    {
        // Rehash password if needed
    }
}

Laravel Sanctum

Sanctum предоставляет лёгкую систему аутентификации для SPA, мобильных приложений и простых API на токенах.

Установка

php artisan install:api

API Token Authentication

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;

final class User extends Authenticatable
{
    use HasApiTokens;
    // ...
}
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

final class TokenController extends Controller
{
    public function createToken(Request $request): JsonResponse
    {
        $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
            'device_name' => ['required', 'string'],
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['Неверные учётные данные.'],
            ]);
        }

        // Create token with abilities (permissions)
        $token = $user->createToken(
            $request->device_name,
            ['orders:read', 'orders:write'] // Token abilities
        );

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

    public function revokeTokens(Request $request): JsonResponse
    {
        // Revoke current token
        $request->user()->currentAccessToken()->delete();

        // Revoke all tokens
        $request->user()->tokens()->delete();

        // Revoke a specific token
        $request->user()->tokens()->where('id', $tokenId)->delete();

        return response()->json(['message' => 'Tokens revoked']);
    }
}

Проверка abilities

Route::middleware('auth:sanctum')->get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('orders:read')) {
        // Token has the 'orders:read' ability
    }
});

// Or via middleware
Route::middleware(['auth:sanctum', 'ability:orders:read'])->group(function () {
    Route::get('/orders', [OrderController::class, 'index']);
});

// Any of the abilities
Route::middleware(['auth:sanctum', 'ability:orders:read,orders:write'])->group(function () {
    // Token needs orders:read OR orders:write
});

Sanctum может аутентифицировать SPA через cookie-сессии без токенов.

// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
// Frontend SPA: first get CSRF cookie, then login
axios.get('/sanctum/csrf-cookie').then(() => {
    axios.post('/login', {
        email: '[email protected]',
        password: 'password',
    });
});

Sanctum vs Passport

Характеристика Sanctum Passport
OAuth2 Нет Полная поддержка
SPA Auth Да (cookie) Нет (нужен token)
API Tokens Простые, с abilities Полные scope
Сложность Минимальная Значительная
Grant Types Нет Все OAuth2 grant types
Подходит для SPA, мобильные, простые API Сторонние приложения, OAuth2 серверы
Токены Хранятся в БД (SHA-256) JWT (опционально)

Когда использовать Sanctum

  • Собственное SPA + API
  • Мобильное приложение + API
  • Простая токенная аутентификация
  • Нет нужды в OAuth2

Когда использовать Passport

  • Стороннее приложение должно получить доступ к вашему API
  • Нужен полный OAuth2 сервер
  • Нужны grant types (authorization code, client credentials)

Middleware аутентификации

// Protect routes
Route::middleware('auth')->group(function () {
    // Only authenticated users
});

// Specific guard
Route::middleware('auth:sanctum')->group(function () {
    // Only users authenticated via Sanctum
});

// Multiple guards
Route::middleware('auth:web,api')->group(function () {
    // Either web or API auth
});

// Guest only
Route::middleware('guest')->group(function () {
    Route::get('/login', [LoginController::class, 'showForm']);
});

// Redirect if authenticated
Route::middleware('auth')->group(function () {
    // Unauthenticated users are redirected to login
    // Customizable in app/Http/Middleware/Authenticate.php
});

События аутентификации

use Illuminate\Auth\Events\Attempting;
use Illuminate\Auth\Events\Authenticated;
use Illuminate\Auth\Events\CurrentDeviceLogout;
use Illuminate\Auth\Events\Failed;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Auth\Events\OtherDeviceLogout;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Events\Validated;
use Illuminate\Auth\Events\Verified;

// Listen for login events
Event::listen(Login::class, function (Login $event) {
    Log::info('User logged in', [
        'user_id' => $event->user->id,
        'guard' => $event->guard,
        'remember' => $event->remember,
    ]);
});

// Listen for failed attempts
Event::listen(Failed::class, function (Failed $event) {
    Log::warning('Failed login attempt', [
        'credentials' => $event->credentials['email'] ?? 'unknown',
        'guard' => $event->guard,
    ]);
});

Тестирование аутентификации

<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\User;
use Tests\TestCase;

final class AuthenticationTest extends TestCase
{
    public function test_user_can_login(): void
    {
        $user = User::factory()->create();

        $response = $this->postJson('/login', [
            'email' => $user->email,
            'password' => 'password', // Factory default
        ]);

        $this->assertAuthenticated();
        $response->assertRedirect('/dashboard');
    }

    public function test_acting_as_user(): void
    {
        $user = User::factory()->create();

        // Simulate authenticated user
        $response = $this->actingAs($user)
            ->getJson('/api/profile');

        $response->assertOk();
    }

    public function test_acting_as_with_guard(): void
    {
        $user = User::factory()->create();

        // Authenticate on specific guard
        $response = $this->actingAs($user, 'sanctum')
            ->getJson('/api/orders');

        $response->assertOk();
    }

    public function test_sanctum_token(): void
    {
        $user = User::factory()->create();
        $token = $user->createToken('test', ['orders:read']);

        $response = $this->withHeader('Authorization', 'Bearer ' . $token->plainTextToken)
            ->getJson('/api/orders');

        $response->assertOk();
    }
}

Проверь себя

Зачем вызывать request->session()->regenerate() после успешной аутентификации?

В чём ключевое различие между Guard и Provider в системе аутентификации Laravel?

Какое свойство Token Abilities в Sanctum позволяет ограничить действия токена?

Как Sanctum аутентифицирует SPA-приложения?

Что делает метод Auth::once() в Laravel?