Account model
Each guard’s model implements RoundlyConsulting\Auth\Contracts\Account and uses the HasAuthentication trait. This is what php artisan authentication:guard clients generates (--no-two-factor / --no-passkeys drop the matching lines; a renamed authentication.columns.password / .email_verified_at replaces that name in $hidden and casts()):
use Illuminate\Auth\Authenticatable as AuthenticatableConcern;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Notifications\Notifiable;
use RoundlyConsulting\Auth\Concerns\HasAuthentication;
use RoundlyConsulting\Auth\Contracts\Account;
use RoundlyConsulting\Passkeys\Concerns\InteractsWithPasskeys;
use RoundlyConsulting\Passkeys\Contracts\HasPasskeys;
use RoundlyConsulting\RefreshTokens\Traits\HasRefreshTokens;
use RoundlyConsulting\TwoFactor\Concerns\HasTwoFactorAuthentication;
use RoundlyConsulting\TwoFactor\Contracts\TwoFactorAuthenticatable;
final class Client extends Model implements Account, HasPasskeys, TwoFactorAuthenticatable
{
use AuthenticatableConcern;
use HasAuthentication;
use HasFactory;
use HasRefreshTokens;
use HasTwoFactorAuthentication;
use InteractsWithPasskeys;
use Notifiable;
use SoftDeletes;
protected $table = 'clients';
protected $guarded = [];
protected $hidden = ['password'];
protected function casts(): array
{
return [...$this->authenticationCasts(), ...$this->twoFactorCasts(), 'email_verified_at' => 'datetime'];
}
}TwoFactorAuthenticatable is required when the guard’s 2FA mode is not off, HasPasskeys when passkeys are on (or passkey login is enabled) — authentication:check verifies it.
The Account contract
| Method | Returns |
|---|---|
accountEmail() | ?string |
hasPassword() | bool |
hasVerifiedEmail() / markEmailAsVerified() | bool |
tokenVersion() | int — the tv claim; bumped on invalidation |
isDisabled() | bool |
isLocked() | bool — locked_until in the future |
accountLocale() / accountTimezone() | ?string |
The HasAuthentication trait
- Implements the contract over the configurable authentication.columns.*.
- authenticationCasts() — spread into casts(): token_version and failed_login_count as integers; password_changed_at, last_login_at, disabled_at and locked_until as immutable datetimes.
- accountEmailColumn(), lockedUntil() and preferredLocale() — Laravel localises the account’s notifications with the latter.
- Hides the password, token_version, locked_until, disabled_reason and failed_login_count columns from serialisation under their configured authentication.columns.* names — a renamed password column stays hidden even next to a stock $hidden = ['password'].
Account columns
The authenticationColumns() Blueprint macro adds token_version, locale, timezone, password_changed_at, last_login_at, disabled_at, disabled_reason, locked_until and failed_login_count (written only when lockout is on). The table also needs email (unique), email_verified_at and password (nullable for passwordless guards), plus two-factor’s and passkeys’ columns when used:
Schema::create('clients', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable();
$table->authenticationColumns();
$table->twoFactorColumns(); // two-factor-for-laravel
$table->passkeyUserHandle(); // passkeys-for-laravel
$table->timestamps();
$table->softDeletes();
});Static analysis
authenticationColumns(): void is declared on Blueprint for Larastan / PHPStan. A macro exists only once its service provider has run. Larastan boots an auto-discovering host app and usually finds it, but not when the provider isn’t loaded during analysis — package development under Testbench, dont-discover, a provider registered by hand. There, a migration calling the macro would fail with “Call to an undefined method”. The package ships a PHPStan extension (RoundlyConsulting\Auth\PHPStan\AuthenticationMacrosExtension) that phpstan/extension-installer loads through extra.phpstan.includes — nothing to configure. Without the installer, include it yourself:
includes:
- vendor/roundly-consulting/auth-for-laravel/extension.neontwo-factor-for-laravel and passkeys-for-laravel declare twoFactorColumns() and passkeyUserHandle() the same way, each through its own extension.neon.
Finding accounts
The AccountRepository is the only class that queries a guard’s model. Soft-deleted accounts never log in but keep their address taken:
$accounts = Authentication::guard('users')->accounts();
$accounts->findForLogin('[email protected]'); // tries identifier.columns in order
$accounts->findByEmail('[email protected]'); // ?Account
$accounts->findByKey(42); // ?Account
$accounts->emailTaken('[email protected]'); // includes soft-deleted accountsAccount lifecycle
use RoundlyConsulting\Auth\DataTransferObjects\LocaleData;
use RoundlyConsulting\Auth\Facades\Authentication;
$users = Authentication::guard('users');
$users->disable($user, 'chargeback fraud'); // every session ends; login and refresh refused
$users->enable($user);
$until = $users->lock($user, 3600); // blocks new logins, sessions stay; the owner is notified
$users->unlock($user);
$users->updateLocale($user, new LocaleData(locale: 'sk', timezone: 'Europe/Bratislava'));
$users->logoutEverywhere($user);- Disable — sets disabled_at and disabled_reason, invalidates everything (always all) and fires AccountDisabled. The account can no longer log in or refresh.
- Lock — locked_until = now + seconds (default lockout.duration). Sessions stay alive; a lock only stops new logins, and the owner is notified as with the automatic lockout. Returns the unlock time.
- Locale — validates the locale against locale.supported and the timezone; fires LocaleUpdated.
- Logout everywhere — bumps the token version and revokes every session; also available as php artisan authentication:logout-everywhere {guard} {id}.
Pass the admin as actor: (any saved model) and it goes on AccountDisabled, AccountEnabled, AccountLocked and AccountUnlocked as actor, ready for an audit log; lock() also tells the owner an administrator locked the account. Disable, enable and unlock send no mail. The account itself counts as no actor; an unsaved model throws InvalidArgumentException before anything is written:
Authentication::disable($user, 'chargeback fraud', actor: $admin);
Authentication::lock($user, seconds: 3600, actor: $admin);
Authentication::unlock($user, actor: $admin);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 cryptoBy 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.