Skip to main content
The basic guard driver provides stateless HTTP Basic authentication backed by the same identity and principal pipeline as the JWT guard. It reads credentials from the standard Authorization: Basic header, validates them through a timing-safe path to prevent user enumeration, resolves the acting principal, and exposes the full contextual surface (Auth::identity(), Auth::principal(), Auth::tenant()) on every authenticated request.

Registering the basic guard

Add a guard entry to config/auth.php with driver: basic and point it at a provider:
'guards' => [
    'cli' => [
        'driver'   => 'basic',
        'provider' => 'users',
    ],
],

'providers' => [
    'users' => [
        'driver' => 'model',
        'model'  => App\Models\User::class,
    ],
],
Protect routes by applying the guard as middleware:
Route::middleware('auth:cli')->group(function () {
    Route::get('/status', StatusController::class);
});

How it works

On each request, the guard reads PHP_AUTH_USER and PHP_AUTH_PW from the PHP superglobals (via Request::getUser() / Request::getPassword()). If either is absent or empty, the guard returns null from Auth::user() without firing any events. When credentials are present, the full pipeline runs inside a Timebox:
  1. Attempting event fires.
  2. The identity provider looks up the record by the configured identifier_field.
  3. Laravel’s hasher compares the supplied password against the stored hash.
  4. If CanBeActive is implemented, isActive() is checked on the identity.
  5. The principal resolver runs and isActive() is checked on the resolved principal.
  6. On success, the identity and principal are bound to the guard and Auth::user() returns the identity.
  7. On any failure, Failed fires and the guard returns null.
The entire pipeline runs within a constant-time Timebox window (default 400 ms) so the response time does not reveal whether the username exists.
When running PHP behind PHP-FPM and nginx, the Authorization header is not automatically forwarded into PHP_AUTH_USER / PHP_AUTH_PW. Without explicit forwarding, the guard sees no credentials on every request and always returns null. Add the following directive to your nginx fastcgi block:
location ~ \.php$ {
    fastcgi_pass_header Authorization;
    # ...rest of the fastcgi block
}
Apache with mod_php populates these superglobals automatically — the gotcha is specific to FastCGI transports. If your basic guard appears to never authenticate, this is the most likely cause.

The identifier_field config

By default, the basic guard looks up identities by the email column. You can change this app-wide in authentication.php:
'credentials' => [
    'identifier_field' => 'email',
],
Any guard can override this with an identifier_field key in its config/auth.php entry. The per-guard value takes precedence over the package default.

Multiple basic guards with different identifier fields

You can register multiple basic guards backed by different providers and keyed by different columns. A common pattern is an email-keyed guard for human users alongside a key_id-keyed guard for per-tenant service credentials:
'guards' => [
    'cli' => [
        'driver'   => 'basic',
        'provider' => 'users',
        // identifier_field omitted — falls back to `authentication.credentials.identifier_field` (default `email`)
    ],
    'tenant_api' => [
        'driver'           => 'basic',
        'provider'         => 'tenant_api_keys',
        'identifier_field' => 'key_id',
    ],
],

'providers' => [
    'tenant_api_keys' => [
        'driver' => 'model',
        'model'  => App\Models\TenantApiKey::class,
    ],
],
TenantApiKey implements Identity (and Principal in 2D mode). Its key_id column is what clients supply as the HTTP Basic username, and its password column holds the bcrypt hash of the API secret. The guard hashes and verifies the supplied secret using Laravel’s configured hasher, exactly as it does for user passwords.

Timing constant

The basic guard wraps the entire credential validation pipeline in Laravel’s Timebox to prevent timing side-channels. The minimum elapsed time is controlled by:
'timebox' => [
    'credentials_microseconds' => 400000, // 400 ms default
],
This value must exceed the worst-case cost of your configured password hasher. If you increase bcrypt rounds or switch to Argon2, measure the maximum hash time under load and increase credentials_microseconds accordingly. Setting it too low defeats the timing-safety guarantee and re-introduces the user-enumeration side-channel the timebox is designed to close.