Skip to main content
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:
config/authentication.php
'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

secret
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:
.env
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.
keys
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.
config/authentication.php
'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.
active_kid
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.
.env
AUTHENTICATION_JWT_ACTIVE_KID=2026-04
algorithm
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
.env
AUTHENTICATION_JWT_ALGORITHM=HS256
access_ttl_minutes
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.
.env
AUTHENTICATION_JWT_ACCESS_TTL_MINUTES=15
refresh_ttl_minutes
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.
.env
AUTHENTICATION_JWT_REFRESH_TTL_MINUTES=43200
leeway_seconds
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.
.env
AUTHENTICATION_JWT_LEEWAY_SECONDS=30
issuer
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.
.env
AUTHENTICATION_JWT_ISSUER=https://api.example.com
audience
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.
.env
AUTHENTICATION_JWT_AUDIENCE=api

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:
1

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

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

Retire the old kid

Once all tokens signed under the old kid have expired, remove it from the keys map and redeploy.
config/authentication.php
'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:
config/auth.php
'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:
$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.
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.