Skip to main content
Device tracking associates each token issuance with a persisted device record in your database. It powers refresh-token rotation — each device row stores a hashed rotation digest that must match the presented refresh token, enabling replay detection and per-device revocation. When a device record is resolved on the bearer path, the package debounces last_logged_in_at writes to avoid hot-spot database pressure on high-traffic APIs. Device tracking is opt-in. If you only need short-lived access tokens without refresh (the common pattern for M2M APIs), you can skip the migration and the HasDevices contract entirely. See access-only mode below.

Config block

config/authentication.php
'device' => [
    'model'                      => env('AUTHENTICATION_DEVICE_MODEL', \SineMacula\Laravel\Authentication\Models\Device::class),
    'table'                      => env('AUTHENTICATION_DEVICE_TABLE', 'devices'),
    'refresh_key_column'         => env('AUTHENTICATION_DEVICE_REFRESH_KEY_COLUMN', 'refresh_key'),
    'last_seen_throttle_seconds' => (int) env('AUTHENTICATION_DEVICE_LAST_SEEN_THROTTLE_SECONDS', 60),
],

Fields

model
string
The fully-qualified class name of the Eloquent model used for device persistence. Sourced from AUTHENTICATION_DEVICE_MODEL. The default is the package’s shipped Device model. Swap this to a custom class when you need additional columns, relationships, or behaviour — the custom model must implement the EloquentDevice contract.
.env
AUTHENTICATION_DEVICE_MODEL=App\Models\Device
table
string
default:"devices"
The database table that stores device records. Sourced from AUTHENTICATION_DEVICE_TABLE. Override this when your schema uses a different table name. The shipped Device model reads this value lazily on each instantiation, so runtime config swaps (such as in multi-tenancy scenarios or tests) take effect immediately.
.env
AUTHENTICATION_DEVICE_TABLE=devices
refresh_key_column
string
default:"refresh_key"
The column on the device table that stores the hashed refresh rotation digest. Sourced from AUTHENTICATION_DEVICE_REFRESH_KEY_COLUMN. The package reads and writes this column during refresh-token rotation; it must exist in the migration and be nullable (it is null before a refresh token has been issued for a device).
.env
AUTHENTICATION_DEVICE_REFRESH_KEY_COLUMN=refresh_key
last_seen_throttle_seconds
number
default:"60"
The minimum number of seconds between last_logged_in_at writes for a given device on the bearer path. When a bearer request resolves a persisted device, the package updates last_logged_in_at only if the stored value is older than this threshold. Set to 0 to write on every request (not recommended on high-traffic APIs). Set to a larger value to reduce write frequency further.
.env
AUTHENTICATION_DEVICE_LAST_SEEN_THROTTLE_SECONDS=60

The shipped Device model

The package provides SineMacula\Laravel\Authentication\Models\Device as the default device adapter. Key characteristics:
  • UUID v7 primary key — generated on insert via Laravel’s HasUuids concern, so device IDs are time-ordered and globally unique without a sequence dependency.
  • Polymorphic authenticatable relation — a morphTo that can point at any identity model, so one devices table serves multiple authenticatable types in the same app.
  • Not final — you can subclass it to add columns or relationships while retaining the built-in EloquentDevice behaviour.
The model’s fillable attributes map directly to the columns created by the published migration:
ColumnTypeNotes
iduuidUUID v7, primary key
authenticatable_typestringMorph type
authenticatable_idstringMorph ID
osstringClient operating system label
refresh_keystring|nullHashed rotation digest; null until first refresh
revoked_attimestamp|nullNon-null means the device is revoked
last_logged_in_attimestamp|nullDebounced last-seen timestamp
last_mfa_verified_attimestamp|nullOptional MFA timestamp

Publishing and running the migration

php artisan vendor:publish --tag=authentication-migrations
php artisan migrate
This creates the devices table with the schema above. If you change device.table, update the migration’s Schema::create call before running it.

Using a custom device model

To use a custom device model, create a class that implements SineMacula\Laravel\Authentication\Contracts\EloquentDevice and point device.model at it:
config/authentication.php (or .env)
AUTHENTICATION_DEVICE_MODEL=App\Models\CustomDevice
The simplest path is to subclass the shipped model:
app/Models/CustomDevice.php
use SineMacula\Laravel\Authentication\Models\Device;

class CustomDevice extends Device
{
    protected $fillable = [
        ...parent::$fillable,
        'push_token',
        'platform',
    ];
}
If you need a fully independent implementation, implement EloquentDevice directly. The contract requires the persistence accessors the package needs for rotation and last-seen tracking — refer to the contract interface for the required method signatures.

Enabling device tracking on your identity model

To opt into device tracking and refresh-token rotation, implement HasDevices on your identity model and define the devices() morph-many relationship:
app/Models/User.php
use Illuminate\Database\Eloquent\Relations\MorphMany;
use SineMacula\Laravel\Authentication\Contracts\HasDevices;
use SineMacula\Laravel\Authentication\Models\Device;

class User extends \Illuminate\Foundation\Auth\User implements Identity, Principal, HasDevices
{
    use Authenticatable, ActsAsPrincipal;

    public function devices(): MorphMany
    {
        return $this->morphMany(Device::class, 'authenticatable');
    }
}
Without HasDevices, the guard skips device resolution and refresh is unavailable.

Access-only mode

Device tracking and refresh-token rotation are entirely optional. If your use case only requires short-lived access tokens — common for M2M APIs, internal service calls, and simple backends — you can skip the devices table and the HasDevices contract.To run in access-only mode:
  1. Publish only the config: php artisan vendor:publish --tag=authentication-config. Skip the authentication-migrations tag.
  2. Do not implement HasDevices on your identity model.
  3. Issue access tokens with null as the device argument: Auth::jwt('api')->issueAccessToken($identity, $principal, null).
  4. Do not call $guard->refresh(...). Clients re-authenticate when their access token expires.
In this mode the package never touches a devices table. Auth::device() returns null, but the full contextual surface (Auth::identity(), Auth::principal(), Auth::tenant(), Auth::type()) still works normally. You can add the migration and refresh flow later — it is additive, not a rewrite.