NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

The package works with zero configuration — every key has a sensible env-backed default. The published config/refresh-tokens.php:

use RoundlyConsulting\RefreshTokens\Enums\DeviceType;
use RoundlyConsulting\RefreshTokens\Models\RefreshToken;

return [
    // Storage
    'table' => env('REFRESH_TOKENS_TABLE', 'refresh_tokens'),
    'model' => RefreshToken::class,
    'device_type_cast' => DeviceType::class,
    'key_type' => env('REFRESH_TOKENS_KEY_TYPE', 'bigint'),

    // Token lifetime & shape
    'ttl' => env('REFRESH_TOKENS_TTL', 2_592_000),                   // seconds; 30 days (sliding)
    'absolute_ttl' => env('REFRESH_TOKENS_ABSOLUTE_TTL', 7_776_000), // seconds; 90 days
    'token_length' => env('REFRESH_TOKENS_LENGTH', 64),              // min 32, max 4096

    // Hashing at rest
    'hash' => [
        'algo' => env('REFRESH_TOKENS_HASH_ALGO', 'sha256'),
        'key' => env('REFRESH_TOKENS_HASH_KEY'), // optional HMAC pepper; null = plain hash
    ],

    // Rotation / anti-replay
    'rotation' => [
        'grace' => env('REFRESH_TOKENS_ROTATION_GRACE', 0),
    ],

    // Expiry sweeping (the host schedules the command / model:prune)
    'prune' => [
        'after' => env('REFRESH_TOKENS_PRUNE_AFTER', 30), // days past revoke/expiry; at least 1
    ],
];

Every key

KeyDefaultEnvPurpose
tablerefresh_tokensREFRESH_TOKENS_TABLETable name. A blank value is not set, so refresh_tokens applies; a non-string value throws.
modelRefreshToken::class—Token model; swap for a subclass of the package model. Anything that is not RefreshToken or a subclass throws.
device_type_castDeviceType::class—Eloquent cast for device_type — the DeviceType enum by default; set 'string' (or any Eloquent cast) to store a free-form device name verbatim. A blank value is not set, so the DeviceType enum applies; a non-string value throws.
key_typebigintREFRESH_TOKENS_KEY_TYPEPrimary-key type shared by every owner model: bigint, uuid or ulid (blank is not set, so bigint applies). Any other value throws InvalidConfigurationException.
ttl2592000 (30 days)REFRESH_TOKENS_TTLDefault sliding lifetime per issue/rotation, in seconds. At least 1.
absolute_ttl7776000 (90 days)REFRESH_TOKENS_ABSOLUTE_TTLDefault absolute cap on a session, stored when the family is rooted. 0 disables; a negative value throws.
token_length64REFRESH_TOKENS_LENGTHPlaintext length in base64url chars (~384 bits at 64). 32–4096, otherwise it throws.
hash.algosha256REFRESH_TOKENS_HASH_ALGOAt-rest digest; allowlisted to sha256, sha384 and sha512 (blank is not set, so sha256 applies; anything else throws).
hash.keynullREFRESH_TOKENS_HASH_KEYOptional HMAC pepper; not set (null or blank) = plain hash, a non-string throws.
rotation.grace0REFRESH_TOKENS_ROTATION_GRACESeconds a re-presented token stays benign before it counts as reuse. 0 = strict; negative throws.
prune.after30REFRESH_TOKENS_PRUNE_AFTERDays to retain rows past revoke/expiry before pruning. At least 1 day; below that throws.

Environment

Every key except model and device_type_cast reads from env, so you rarely publish the config at all:

REFRESH_TOKENS_TABLE=refresh_tokens
REFRESH_TOKENS_KEY_TYPE=bigint
REFRESH_TOKENS_TTL=2592000
REFRESH_TOKENS_ABSOLUTE_TTL=7776000
REFRESH_TOKENS_LENGTH=64
REFRESH_TOKENS_HASH_ALGO=sha256
REFRESH_TOKENS_HASH_KEY=
REFRESH_TOKENS_ROTATION_GRACE=0
REFRESH_TOKENS_PRUNE_AFTER=30

Validated configuration

Every key fails loud instead of being silently coerced, so an env typo can’t degrade the token store:

  • hash.algo is restricted to the SHA-2 allowlist (sha256, sha384, sha512). A blank value is not set, so sha256 applies; anything else — md5, crc32b, … — throws InvalidTokenConfigurationException.
  • token_length must be between 32 and 4096 characters; outside that range it throws instead of minting a brute-forceable token.
  • Every integer — ttl, absolute_ttl, token_length, rotation.grace and prune.after — is read strictly: an int or a canonical integer string ('30', as env values arrive). A key that is not set — absent, null or blank ('', what KEY= in .env gives) — takes its default; 'five', '1.5' or a value out of range throws InvalidTokenConfigurationException naming the key — never a silent 0 or default.
  • table and device_type_cast must be strings; a blank one is not set, so its default applies. hash.key may be null or blank (no pepper), but a value that is not a string at all throws rather than silently dropping the pepper.
  • key_type accepts bigint, uuid or ulid; anything else throws InvalidConfigurationException naming the key and the value. A typo such as guid would otherwise build a bigint owner_id for uuid or ulid owners, and nothing would fail until rows stopped joining. id, the package’s spelling before it adopted the shared key type, is no longer accepted — use bigint.
  • model must be RefreshToken or a subclass of it; anything else throws InvalidConfigurationException naming the key instead of silently falling back to the packaged model.

Device-type cast

device_type is cast to the packaged DeviceType enum (desktop, mobile, tablet, bot, unknown) by default. If your user-agent parser emits a richer, free-form vocabulary, store it verbatim with any Eloquent cast string:

// config/refresh-tokens.php — store a free-form device vocabulary verbatim
'device_type_cast' => 'string',

Inspecting the live configuration

php artisan about --only=refresh-tokens

The about section is secret-safe — nothing a support screenshot could leak:

  • Token model, Owner (the morph key type), Sliding TTL, Absolute TTL (or DISABLED), Token length, Hash algorithm, Rotation grace (or STRICT) and Prune after. Sliding TTL, Absolute TTL, Token length, Hash algorithm, Rotation grace and Prune after read INVALID when the value is malformed — about reports it instead of throwing.
  • Table reports DEFAULT or CUSTOM, never the name.
  • Hash pepper reports SET or MISSING, never the value.
  • Access-token revoker reports NONE (no-op) or BOUND.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.