Access tokens are self-contained: once issued, they remain cryptographically valid until they expire. If you suspend a user after issuing their token, the guard has no way to know about the suspension unless you give it a live signal. The CanBeActive contract provides exactly that — a per-request active-state check that both guards consult on every authentication attempt, with no dependency on token expiry or session invalidation.
What CanBeActive does
CanBeActive is an optional capability contract that you implement on your identity model. When both guards — the JWT bearer guard and the HTTP Basic guard — resolve an identity, they check whether the model implements CanBeActive. If it does, they call isActive() and reject authentication immediately when it returns false.
The check runs on every request, regardless of whether resolution caching is enabled. Caching only short-circuits the database lookup for the identity row itself; the active-state check always runs against the in-memory model returned by that lookup.
Why use it
Without CanBeActive, the only way to block a compromised or suspended account is to wait for all outstanding access tokens to expire. If your access_ttl_minutes is set to 60 minutes, a suspended user retains access for up to an hour after suspension.
With CanBeActive, the guard rejects the request as soon as the check fails, regardless of how long the token has left to live. This is the preferred approach for:
- Suspended accounts — a user flagged by a support workflow
- Banned accounts — a user blocked for policy violations
- Soft-deleted accounts — a user whose record is deleted but whose token is still in circulation
- Administratively locked accounts — a user locked pending a security review
Implementation
Implement CanBeActive on your identity model and return a boolean from isActive():
use SineMacula\Laravel\Authentication\Contracts\CanBeActive;
use SineMacula\Laravel\Authentication\Contracts\Identity;
use SineMacula\Laravel\Authentication\Contracts\Principal;
use SineMacula\Laravel\Authentication\Traits\ActsAsPrincipal;
use SineMacula\Laravel\Authentication\Traits\Authenticatable;
use Illuminate\Foundation\Auth\User;
class AppUser extends User implements Identity, Principal, CanBeActive
{
use Authenticatable, ActsAsPrincipal;
public function isActive(): bool
{
return $this->suspended_at === null;
}
}
No registration is required beyond implementing the interface. Both guards detect the contract via instanceof on the resolved identity.
Combine CanBeActive with Laravel’s soft deletes by checking both suspended_at and deleted_at in the same method: return $this->suspended_at === null && $this->deleted_at === null;. If your model uses SoftDeletes, queries that scope to non-deleted records will already exclude soft-deleted users from the provider lookup — isActive() adds a second line of defence at the guard level in case the record is loaded through a non-scoped query path.
Blocking inactive principals
The active-state check is not limited to identities. The guard also checks the resolved principal: if the principal implements CanBeActive and its isActive() returns false, authentication fails at the principal resolution step.
This matters in 3D adoption mode, where the principal is a separate model (such as a tenant membership) that can be independently suspended. A user whose identity is active but whose membership in a particular tenant has been revoked is blocked from acting as that principal, even if their identity token is otherwise valid.
What happens on failure
When isActive() returns false for either the identity or the principal, the guard fires Laravel’s standard Illuminate\Auth\Events\Failed event and returns null from Auth::user(). The request surface is a standard 401 response — no information about the reason for rejection is leaked to the caller.
Active-state and the resolution cache
The resolution cache, when enabled, only short-circuits the identity provider lookup (the database query). The active-state check runs against the model that the provider returns — cached or not — on every request. You cannot disable or bypass the isActive() call by enabling caching.
Similarly, device validation and principal resolution always run live. There is no configuration path that causes the guard to skip the active-state check.