> ## 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.

# Security Model and Hardening Defaults

> Learn how Laravel Authentication's JWT pipeline, refresh rotation, and fail-closed defaults protect your app against common attack vectors.

The package is built around a fail-closed philosophy: misconfiguration raises an exception at resolution time rather than silently degrading to an insecure state. This page documents the specific security properties the package enforces, where you must supply additional controls, and the caveats that apply to access-only deployments.

## Fail-closed defaults

An empty or invalid JWT secret causes the guard to throw `InvalidJwtConfigurationException` the first time it is resolved — not at boot time, and not silently. There is no fallback to an insecure mode, no warning-and-continue, and no way to configure a guard that accepts forged tokens when signing material is missing.

The same applies to kid mode: an `active_kid` that is not present in the `keys` map, an empty `keys` map, or any entry in the map whose secret is an empty string all throw at keyring construction time. Every signing-material boundary is fail-closed regardless of configuration depth.

## JWT pipeline hardening

Every token parse enforces the full claim set:

* **`iss`** — issuer must match the configured issuer for the guard
* **`aud`** — audience must match the configured audience for the guard
* **`typ`** — token type must match the expected type for the path (access token on bearer, refresh token on refresh exchange)
* **`exp`** — token must not be expired, subject to the configured leeway
* **`leeway`** — clock-skew tolerance is explicit and bounded; it does not disable expiry checking

The `typ` claim prevents type-confusion attacks where a refresh token is submitted on the bearer path or vice versa. A refresh token presented to the bearer path is rejected because its `typ` does not match the expected access-token type, even if it is otherwise cryptographically valid.

Every issued token embeds a per-token `jti` (JWT ID). The `jti` is carried through the refresh exchange but is not consulted on the bearer path — see the access-only mode section below.

## Fail-closed `pid` and `did` claims

A token that carries a `pid` (principal ID) or `did` (device ID) claim in its payload is not allowed to silently downgrade to a no-principal or no-device state if resolution fails.

* If a `pid` claim is present and the principal resolver cannot return a matching principal, the request is rejected.
* If a `did` claim is present and the device record cannot be loaded, the request is rejected.
* Neither claim degrades gracefully to `null`. The guard never silently strips a claim and proceeds as if it were absent.

This prevents a class of attack where a tampered or replayed token with a mismatched `pid` causes the guard to fall back to a default principal the identity was not authorized to act as.

## Refresh-token replay detection

Refresh-token rotation uses an atomic per-device compare-and-swap on the stored rotation digest. The rotation digest is a constant-time hash of the issued refresh token; it is updated atomically when a new token pair is issued.

If two requests attempt to use the same refresh token concurrently — or if an attacker replays a previously consumed refresh token — the CAS fails. The device is immediately revoked and a `RefreshFailed` event is dispatched with reason `ROTATION_REUSE`. Once revoked, no further refresh exchanges are possible for that device; the user must re-authenticate from scratch.

This property means that a compromised refresh token can be detected and neutralized the moment the legitimate client next attempts to refresh, because the attacker's use of the token will have invalidated the client's copy and vice versa.

## Constant-time credential validation

The HTTP Basic guard wraps the entire credential validation pipeline — provider lookup, password hash comparison, active-state check, and principal resolution — inside Laravel's `Timebox`. The minimum elapsed wall-clock time is configurable via `timebox.credentials_microseconds` (default 400 ms).

The timebox ensures that responses arrive in approximately constant time regardless of whether the username exists in the database. Without this control, an attacker could distinguish valid usernames from invalid ones by measuring response latency — a user-enumeration side-channel.

The `credentials_microseconds` value must exceed the worst-case cost of your configured hasher under load. If you increase bcrypt rounds or switch to Argon2, re-measure and raise the limit accordingly.

<Note>
  The resolution cache, when enabled, only short-circuits the bearer identity provider lookup — the database query that fetches the identity row. Active-state checks (`CanBeActive::isActive()`), `pid` claim matching, and device validation always run live against the resolved model on every request. The refresh path never uses the cache at all: revocation and replay detection are device-backed and immediate. Enabling the cache does not weaken any of these checks; it only affects how the identity row itself is fetched.
</Note>

## Revoking access

Revoking a device — by deleting or flagging its row in the `devices` table — blocks refresh immediately. Any subsequent refresh attempt for that device will fail with `DEVICE_REVOKED` and fire a `RefreshFailed` event.

<Warning>
  Revoking a device does **not** invalidate already-issued access tokens. Access tokens are self-verifying JWTs: the guard validates them against their signature and claims without consulting any server-side store. A revoked device's access tokens remain valid until their `exp` claim passes. For sensitive contexts, keep `access_ttl_minutes` short (15 minutes or less) so the revocation window is bounded. If you need immediate invalidation, you must implement a token blocklist at the application layer — the package does not ship one.
</Warning>

## Access-only mode security note

When running in access-only mode (no `did` claim, no refresh, no device tracking), `jti` values embedded in access tokens are not consulted on the bearer path. There is no server-side lookup of `jti` values — revocation can only occur through identity, principal, or device rehydration failure. Specifically:

* If the identity's `isActive()` returns `false`, the request is rejected.
* If the principal resolver returns `null` or the principal's `isActive()` returns `false`, the request is rejected.
* If the `pid` or `did` claim does not resolve, the request is rejected.

Outside of those conditions, a token that is cryptographically valid and has not expired will authenticate successfully, regardless of any application-level state changes that occurred after the token was issued.
