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

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

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

MethodReturns
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 token_version, locked_until, disabled_reason and failed_login_count from serialisation.

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();
});

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 accounts

Account 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}.

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.