Guards and Providers
A guard decides how a request is authenticated, a provider decides where users come from, and both live in config/auth.php.
Auth::guard('admin')->attempt(array $credentials, bool $remember = false): boolA guard decides how a request is authenticated. A provider decides where users are loaded from. Both are named, both are configured in config/auth.php, and most baffling authentication bugs are one of them pointing somewhere you did not expect.
The web guard
The web guard uses the session driver, which is Illuminate\Auth\SessionGuard. It keeps the user's key under a session name built from the guard's name, like login_web_<sha1>, and that per-guard key is why two guards have independent login state. A guard's optional remember key sets how many minutes the remember cookie lasts.
A token guard
The token driver (Illuminate\Auth\TokenGuard) reads an API token from the request and matches it against a column instead. Its config keys are input_key and storage_key, both api_token by default, plus hash. It is stateless: nothing is written to the session, so there is nothing to log out of. Real APIs usually reach for Sanctum, but the token guard shows the shape in twelve lines of config.
Several guards side by side
Name a second guard and an application can authenticate customers on web and staff on admin, each with its own provider and model. Auth::guard('admin')->attempt(...) and the auth:admin route middleware name it; Auth::attempt(...) is shorthand for the guard in config('auth.defaults.guard'), and auth('admin') is shorthand for Auth::guard('admin').
A provider is either eloquent with a model, or database with a table read through the query builder.
Example
<?php
// config/auth.php
declare(strict_types=1);
use App\Models\Admin;
use App\Models\User;
return [
// Auth::user() and Auth::attempt() mean this guard.
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
'guards' => [
// Cookie and session: Illuminate\Auth\SessionGuard.
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// A second session guard. Its own session key, its own users.
'admin' => [
'driver' => 'session',
'provider' => 'admins',
'remember' => 43200, // minutes the remember cookie lives
],
// Stateless: Illuminate\Auth\TokenGuard reads a token from the
// request and matches it against a column.
'api' => [
'driver' => 'token',
'provider' => 'users',
'input_key' => 'api_token', // where it reads the token from
'storage_key' => 'api_token', // the column it compares with
'hash' => false, // true if you store a sha256
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
'admins' => [
'driver' => 'eloquent',
'model' => Admin::class,
],
// The query builder instead of a model. No Eloquent involved:
// 'admins' => [
// 'driver' => 'database',
// 'table' => 'admins',
// ],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60, // minutes a reset token stays valid
'throttle' => 60, // seconds before another link may be asked for
],
],
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];When to use it
- Adding a separate staff login that reads a different table from customers
- Working out why Auth::user() returns null straight after a successful login
- Authenticating an internal endpoint with a shared token instead of a session
- Pointing the default provider at a model that is not App\Models\User
More examples
Using a named guard
The guard argument to the auth middleware is the same name as the config key, and the middleware runs each guard you list until one succeeds: auth:admin,web accepts either. Reach for a second guard when staff accounts live in a different table with different columns. When they are the same users with a flag or a role, one guard plus authorization is the simpler answer.
<?php
// app/Http/Controllers/Admin/SessionController.php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SessionController extends Controller
{
public function store(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
// Name the guard on every call. Auth::attempt() would use 'web'
// and look in the wrong table.
if (! Auth::guard('admin')->attempt($credentials)) {
return back()->withErrors(['email' => 'Those credentials do not match our records.']);
}
$request->session()->regenerate();
// auth('admin') is shorthand for Auth::guard('admin').
$staff = auth('admin')->user();
return redirect()->intended('/admin');
}
public function destroy(Request $request): RedirectResponse
{
// Logs out of this guard only. A customer signed in on 'web'
// in the same browser stays signed in.
Auth::guard('admin')->logout();
return redirect('/admin/login');
}
}
// routes/web.php
//
// Route::middleware('auth:admin')->prefix('admin')->group(function () {
// Route::get('/', DashboardController::class);
// });Following the config by hand
Two hops, every time: guard to provider, provider to model. Walking them by hand is the fastest way to answer "why is Auth::user() an instance of the wrong class", and in a real application the same two lookups are php artisan tinker with config('auth.guards.web') and config('auth.providers.users').
<?php
declare(strict_types=1);
use Illuminate\Support\Arr;
// The parts of config/auth.php that decide which class gets loaded.
$auth = [
'defaults' => ['guard' => 'web'],
'guards' => [
'web' => ['driver' => 'session', 'provider' => 'users'],
'admin' => ['driver' => 'session', 'provider' => 'admins'],
],
'providers' => [
'users' => ['driver' => 'eloquent', 'model' => 'App\Models\User'],
'admins' => ['driver' => 'eloquent', 'model' => 'App\Models\Admin'],
],
];
function resolve_model(array $auth, ?string $guard = null): string
{
// No guard named? Fall back to defaults.guard, as Auth does.
$guard ??= Arr::get($auth, 'defaults.guard');
$provider = Arr::get($auth, "guards.{$guard}.provider");
return Arr::get($auth, "providers.{$provider}.model");
}
echo resolve_model($auth), PHP_EOL;
echo resolve_model($auth, 'admin'), PHP_EOL;
// Output:
// App\Models\User
// App\Models\AdminA guard that is just a closure
viaRequest wraps the closure in Illuminate\Auth\RequestGuard, so you get a real guard without writing a class or a provider: note that this driver needs no 'provider' key, because the closure does the lookup itself. Use it for a webhook endpoint or an internal service token. For anything user-facing, Auth::extend with a Guard implementation, or Sanctum, gives you the rest of the contract.
<?php
// app/Providers/AppServiceProvider.php
declare(strict_types=1);
namespace App\Providers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Auth::viaRequest(name, closure). Return a user, or null to fail.
Auth::viaRequest('custom-token', function (Request $request) {
return User::where('token', (string) $request->token)->first();
});
}
}
// config/auth.php
//
// 'guards' => [
// 'api' => ['driver' => 'custom-token'],
// ],
//
// routes/api.php
//
// Route::middleware('auth:api')->get('/me', fn (Request $r) => $r->user());
Discussion