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

# JWT Configuration Reference

> Configure JWT signing secrets, key rotation, algorithm, TTLs, issuer, audience, and per-guard overrides for Laravel Authentication.

The JWT configuration block in `config/authentication.php` controls every aspect of token signing and verification. All guards using the `jwt` driver read these defaults; individual guards can override any field through a `jwt` sub-block in `config/auth.php`. The service fails closed — if signing material is absent or invalid when a JWT guard or token service is resolved, it throws rather than silently accepting forged tokens.

## Full config block

This is the published `jwt` section of `config/authentication.php`:

```php config/authentication.php theme={null}
'jwt' => [
    'secret' => env('AUTHENTICATION_JWT_SECRET'),

    // Optional `kid => secret` map for graceful key rotation. When
    // populated, takes precedence over `secret` above.
    'keys' => [],

    // Kid in the `keys` map that signs newly issued tokens. Required when
    // `keys` is non-empty.
    'active_kid' => env('AUTHENTICATION_JWT_ACTIVE_KID', ''),

    'algorithm'           => env('AUTHENTICATION_JWT_ALGORITHM', 'HS256'),
    'access_ttl_minutes'  => (int) env('AUTHENTICATION_JWT_ACCESS_TTL_MINUTES', 15),
    'refresh_ttl_minutes' => (int) env('AUTHENTICATION_JWT_REFRESH_TTL_MINUTES', 60 * 24 * 30),
    'leeway_seconds'      => (int) env('AUTHENTICATION_JWT_LEEWAY_SECONDS', 30),
    'issuer'              => env('AUTHENTICATION_JWT_ISSUER'),
    'audience'            => env('AUTHENTICATION_JWT_AUDIENCE'),
],
```

## Fields

<ParamField body="secret" type="string">
  The HMAC signing secret for single-secret mode. Sourced from the `AUTHENTICATION_JWT_SECRET` environment variable. Must be at least 32 bytes of random data; shorter values are technically accepted but weaken the signature. The keyring **fails closed** — an empty or missing secret throws `InvalidJwtConfigurationException` at boot rather than allowing unsigned tokens through.

  Set this in your environment:

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_SECRET="a-strong-random-value-of-at-least-32-bytes"
  ```

  When `keys` is non-empty, `secret` is ignored. You do not need both.
</ParamField>

<ParamField body="keys" type="object" default="[]">
  A `kid → secret` map that enables **kid-based key rotation**. When this map is non-empty, it takes precedence over `secret`. Each entry maps a key identifier string to its HMAC secret. All kids present in the map are accepted on verification; only the `active_kid` is used for signing new tokens.

  ```php config/authentication.php theme={null}
  'keys' => [
      '2026-04' => env('AUTHENTICATION_JWT_KEY_2026_04'),
      '2026-03' => env('AUTHENTICATION_JWT_KEY_2026_03'),
  ],
  ```

  Kids and secrets must be non-empty strings. An empty kid string or empty secret value throws at boot.
</ParamField>

<ParamField body="active_kid" type="string" default="">
  The kid from the `keys` map that signs newly issued tokens. The kid is embedded in the JWT header so the verifier can select the correct key. This field is required when `keys` is non-empty — if `active_kid` is empty or points to a kid that does not exist in `keys`, the keyring throws at construction.

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_ACTIVE_KID=2026-04
  ```
</ParamField>

<ParamField body="algorithm" type="string" default="HS256">
  The JWS signing algorithm. The package allow-lists the following algorithms; any other value throws at boot:

  * `HS256`, `HS384`, `HS512`
  * `RS256`, `RS384`, `RS512`
  * `ES256`, `ES384`

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_ALGORITHM=HS256
  ```
</ParamField>

<ParamField body="access_ttl_minutes" type="number" default="15">
  Lifetime of an access token in minutes, written into the `exp` claim. After this window, plus any `leeway_seconds`, the token is rejected. The default of 15 minutes is deliberately short; access tokens are self-verifying and cannot be revoked server-side without a blocklist.

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_ACCESS_TTL_MINUTES=15
  ```
</ParamField>

<ParamField body="refresh_ttl_minutes" type="number" default="43200">
  Lifetime of a refresh token in minutes, written into the `exp` claim. The default is `43200` (30 days). Refresh tokens are stateful — they are backed by the `devices` table and subject to replay detection regardless of this TTL.

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_REFRESH_TTL_MINUTES=43200
  ```
</ParamField>

<ParamField body="leeway_seconds" type="number" default="30">
  Clock-skew tolerance applied to every `exp` and `iat` check during token verification. A token whose `exp` falls within `leeway_seconds` of the current server time is still accepted. The default of 30 seconds accommodates reasonable clock drift between the issuer and verifier. Do not set this to a large value — it extends every token's effective lifetime.

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_LEEWAY_SECONDS=30
  ```
</ParamField>

<ParamField body="issuer" type="string">
  Optional. When set, the `iss` claim is embedded in every issued token and strictly verified on every parse. Tokens whose `iss` claim does not exactly match this value are rejected. Omit to skip issuer verification entirely.

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_ISSUER=https://api.example.com
  ```
</ParamField>

<ParamField body="audience" type="string">
  Optional. When set, the `aud` claim is embedded in every issued token and strictly verified on every parse. Tokens whose `aud` claim does not exactly match are rejected. This is the primary mechanism for isolating multiple JWT guards — each guard can carry its own `audience` so tokens minted by one guard cannot authenticate against another.

  ```dotenv .env theme={null}
  AUTHENTICATION_JWT_AUDIENCE=api
  ```
</ParamField>

## Key rotation

For production deployments that need graceful signing-key rotation, configure `keys` and `active_kid` instead of `secret`. The verifier accepts every kid present in the map; only `active_kid` signs new tokens. To rotate:

<Steps>
  <Step title="Add the new kid">
    Add the new kid and its secret to the `keys` map. Deploy with `active_kid` still pointing at the old kid — both kids now verify, but new tokens continue to use the old signing key.
  </Step>

  <Step title="Promote the new kid">
    Update `active_kid` to the new kid. New tokens are now signed under the new key; old tokens signed under the previous kid continue to verify until they expire.
  </Step>

  <Step title="Retire the old kid">
    Once all tokens signed under the old kid have expired, remove it from the `keys` map and redeploy.
  </Step>
</Steps>

```php config/authentication.php theme={null}
'jwt' => [
    'keys' => [
        '2026-04' => env('AUTHENTICATION_JWT_KEY_2026_04'),
        '2026-03' => env('AUTHENTICATION_JWT_KEY_2026_03'),
    ],
    'active_kid' => env('AUTHENTICATION_JWT_ACTIVE_KID', '2026-04'),
],
```

## Per-guard JWT overrides

Every `jwt` guard inherits its signing material, audience, issuer, TTLs, and leeway from the package-wide `authentication.jwt.*` defaults. You can override any of these per guard by adding a `jwt` sub-block to the guard's entry in `config/auth.php`. Fields you omit fall back to the package defaults.

This lets you run multiple JWT guards with distinct trust boundaries from a single app — tokens minted by one guard carry that guard's `aud` claim and cannot authenticate against another:

```php config/auth.php theme={null}
'guards' => [
    'staff' => [
        'driver'   => 'jwt',
        'provider' => 'users',
        'jwt'      => [
            'secret'   => env('STAFF_JWT_SECRET'),
            'audience' => 'staff-api',
        ],
    ],
    'customer' => [
        'driver'   => 'jwt',
        'provider' => 'users',
        'jwt'      => [
            'secret'   => env('CUSTOMER_JWT_SECRET'),
            'audience' => 'customer-api',
        ],
    ],
],
```

Routes opt into a specific boundary via `auth:staff` or `auth:customer` middleware. Issue tokens through the guard context so the correct signing material and audience claim are applied:

```php theme={null}
$staffAccessToken = Auth::jwt('staff')->issueAccessToken($identity, $principal, $device);
```

Every field in the package `jwt` block is overridable per guard: `secret`, `keys`, `active_kid`, `algorithm`, `access_ttl_minutes`, `refresh_ttl_minutes`, `leeway_seconds`, `issuer`, and `audience`. Guards can carry fully independent kid-rotation maps if you need separate signing-key lifecycles per trust boundary.

<Tip>
  Per-guard `kid` rotation is independent. Each guard can define its own `keys` map and `active_kid`, so rotating the signing key for `staff` tokens does not affect `customer` tokens.
</Tip>
