> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sinemacula.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Advanced Configuration Options

> Configure bearer identity caching, credential timebox, per-guard principal resolvers, and identifier field overrides for fine-grained control.

Beyond JWT signing and device tracking, the package exposes three additional configuration areas: an optional bearer identity cache that reduces database pressure on high-traffic bearer paths, a timebox that prevents timing side-channels on HTTP Basic credential checks, and per-guard layering for identifier fields and principal resolvers. All three are off or at safe defaults out of the box.

## Resolution cache

Cross-request resolution caching is **disabled by default**. When enabled, it applies exclusively to JWT bearer identity rehydration through model providers — the lookup that turns the `sub` claim in an access token back into a live identity model. Everything else stays live on every request.

**What is cached:** the identity model resolved from the bearer token's `sub` claim.

**What stays live on every request regardless of cache state:**

* HTTP Basic credential lookups
* Bearer-path device resolution (`did` claim)
* Principal resolution (`pid` claim)
* The entire refresh flow — revocation and replay detection remain device-backed and immediate

### Config block

```php config/authentication.php theme={null}
'resolution_cache' => [
    'store' => env('AUTHENTICATION_RESOLUTION_CACHE_STORE'),
    'jwt'   => [
        'identity_ttl_seconds'  => (int) env('AUTHENTICATION_RESOLUTION_CACHE_JWT_IDENTITY_TTL_SECONDS', 0),
        'principal_ttl_seconds' => (int) env('AUTHENTICATION_RESOLUTION_CACHE_JWT_PRINCIPAL_TTL_SECONDS', 0),
    ],
],
```

### Fields

<ParamField body="resolution_cache.store" type="string">
  The Laravel cache store to use for bearer identity caching. Accepts any store name configured in `config/cache.php` (for example, `redis`, `memcached`, `dynamodb`). When left empty or unset, the cache is disabled regardless of TTL settings. You must set both this and `identity_ttl_seconds` to a non-zero value to enable caching.

  ```dotenv .env theme={null}
  AUTHENTICATION_RESOLUTION_CACHE_STORE=redis
  ```
</ParamField>

<ParamField body="resolution_cache.jwt.identity_ttl_seconds" type="number" default="0">
  How long a cached bearer identity entry lives, in seconds. `0` disables the cache entirely. When non-zero, a cache hit on the bearer path skips the provider lookup for the identity model for subsequent requests within this window. Active-state checks, `pid` matching, and `did` device validation still run live on every request.

  ```dotenv .env theme={null}
  AUTHENTICATION_RESOLUTION_CACHE_JWT_IDENTITY_TTL_SECONDS=300
  ```
</ParamField>

<ParamField body="resolution_cache.jwt.principal_ttl_seconds" type="number" default="0">
  Reserved for future use. Keep this at `0`. Principal resolution runs live on every request and is not currently cached.

  ```dotenv .env theme={null}
  AUTHENTICATION_RESOLUTION_CACHE_JWT_PRINCIPAL_TTL_SECONDS=0
  ```
</ParamField>

### Enabling the cache

<Steps>
  <Step title="Configure a cache store">
    Set `AUTHENTICATION_RESOLUTION_CACHE_STORE` to a named store in your `config/cache.php`. A persistent store such as Redis is required — the in-memory `array` driver does not share state across requests.

    ```dotenv .env theme={null}
    AUTHENTICATION_RESOLUTION_CACHE_STORE=redis
    AUTHENTICATION_RESOLUTION_CACHE_JWT_IDENTITY_TTL_SECONDS=300
    ```
  </Step>

  <Step title="Wire explicit invalidation">
    Add a model observer (or equivalent write-path hook) to your identity model that calls `ResolutionCacheInvalidator::forgetIdentity()` whenever the identity is saved or deleted. **This step is mandatory.** Skipping it means cached entries survive identity updates, bans, or deletions until the TTL expires.

    ```php app/Observers/UserObserver.php theme={null}
    use App\Models\User;
    use SineMacula\Laravel\Authentication\Cache\ResolutionCacheInvalidator;

    final class UserObserver
    {
        public function saved(User $user): void
        {
            app(ResolutionCacheInvalidator::class)->forgetIdentity($user);
        }

        public function deleted(User $user): void
        {
            app(ResolutionCacheInvalidator::class)->forgetIdentity($user);
        }
    }
    ```

    Register the observer in a service provider:

    ```php app/Providers/AppServiceProvider.php theme={null}
    use App\Models\User;
    use App\Observers\UserObserver;

    public function boot(): void
    {
        User::observe(UserObserver::class);
    }
    ```
  </Step>

  <Step title="Handle identifier changes">
    If your identity model's auth identifier (typically `email` or `id`) can change, you must also invalidate the **previous** identifier before the save is committed — otherwise the old cache entry remains unaddressed:

    ```php app/Observers/UserObserver.php theme={null}
    public function saving(User $user): void
    {
        if ($user->isDirty($user->getAuthIdentifierName())) {
            $previousIdentifier = $user->getOriginal($user->getAuthIdentifierName());

            app(ResolutionCacheInvalidator::class)->forgetIdentity($user, $previousIdentifier);
        }
    }

    public function saved(User $user): void
    {
        app(ResolutionCacheInvalidator::class)->forgetIdentity($user);
    }
    ```
  </Step>
</Steps>

<Warning>
  Do not enable the resolution cache without the invalidation wiring in place. A cached entry that survives a suspension, ban, or deletion allows the affected identity to continue authenticating on the bearer path until the TTL expires, even when `CanBeActive::isActive()` returns `false`. The active-state check runs live — but only after the identity model is resolved. A stale cache entry short-circuits the provider lookup and delivers a cached model whose in-memory `isActive()` reflects the state at cache-fill time.
</Warning>

***

## Credential timebox

The `basic` guard wraps every credential check in `Illuminate\Support\Timebox` to prevent timing side-channels. Without a timebox, an attacker can distinguish "username not found" (fast) from "wrong password" (slow, bcrypt) by measuring response latency, leaking the existence of accounts.

### Config block

```php config/authentication.php theme={null}
'timebox' => [
    'credentials_microseconds' => (int) env('AUTHENTICATION_TIMEBOX_CREDENTIALS_US', 400000),
],
```

### Field

<ParamField body="timebox.credentials_microseconds" type="number" default="400000">
  The minimum number of microseconds the credential-validation path must take. The default of `400000` (400 ms) is intentionally above the worst-case cost of bcrypt at cost factor 12, which typically runs between 150 ms and 250 ms. If you use a higher bcrypt cost or a different hasher, increase this value to exceed your worst-case hash time.

  ```dotenv .env theme={null}
  AUTHENTICATION_TIMEBOX_CREDENTIALS_US=400000
  ```

  Setting this too low defeats the timing protection. Setting it higher than necessary adds latency to every Basic auth request. Choose a value that exceeds your hasher's measured worst-case cost with a comfortable margin.
</ParamField>

<Tip>
  The timebox only applies to the credential-validation path of the `basic` guard. JWT bearer authentication does not involve password hashing and is not subject to this timing constraint.
</Tip>

***

## Per-guard identifier field and principal resolvers

Both the identifier field used for credential lookups and the principal resolver can be layered per guard, following the same override pattern as JWT config.

### Identifier field

The `credentials.identifier_field` setting controls which column the `basic` guard uses when constructing the credential lookup from the HTTP Basic username:

```php config/authentication.php theme={null}
'credentials' => [
    'identifier_field' => env('AUTHENTICATION_IDENTIFIER_FIELD', 'email'),
],
```

<ParamField body="credentials.identifier_field" type="string" default="email">
  The app-wide default field name passed to the identity provider when looking up credentials from an HTTP Basic username. Override when your identity model keys off `username`, `phone`, `key_id`, or any other column.

  ```dotenv .env theme={null}
  AUTHENTICATION_IDENTIFIER_FIELD=email
  ```
</ParamField>

Any `basic` guard can override this app-wide default with an `identifier_field` entry directly in `config/auth.php`. This lets you register multiple Basic guards backed by different providers and different lookup columns in a single app:

```php config/auth.php theme={null}
'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,
    ],
],
```

The `tenant_api` guard looks up credentials against the `key_id` column on `TenantApiKey`, while `cli` uses the default `email` column on your user model.

### Per-guard principal resolvers

By default, every guard resolves principals via the app-wide `SineMacula\Laravel\Authentication\Contracts\PrincipalResolver` container binding, which falls back to the package's `DefaultPrincipalResolver` when no custom binding is registered. Any guard can override this with a `principal_resolver` entry in `config/auth.php`:

```php config/auth.php theme={null}
'guards' => [
    'staff' => [
        'driver'             => 'jwt',
        'provider'           => 'users',
        'principal_resolver' => App\Auth\Resolvers\StaffPrincipalResolver::class,
    ],
    'customer' => [
        'driver'             => 'jwt',
        'provider'           => 'users',
        'principal_resolver' => App\Auth\Resolvers\CustomerPrincipalResolver::class,
    ],
],
```

Resolution precedence:

1. `auth.guards.<name>.principal_resolver` — guard-local class, highest priority
2. The app-wide `PrincipalResolver::class` container binding
3. The package default `DefaultPrincipalResolver`

The per-guard resolver applies to **both** the bearer-token path and the JWT refresh exchange. When a guard declares a local resolver, both paths use the same resolver instance, so principal resolution is consistent regardless of whether the request is an initial bearer call or a refresh.

To register an app-wide resolver for all guards that do not declare a local one, bind it in a service provider:

```php app/Providers/AppServiceProvider.php theme={null}
use SineMacula\Laravel\Authentication\Contracts\PrincipalResolver;
use App\Auth\Resolvers\MyTenantScopedResolver;

public function register(): void
{
    $this->app->singleton(PrincipalResolver::class, MyTenantScopedResolver::class);
}
```
