Authenticate a user with the core alone, without any UI.
Every capability is an action behind a contract, bound in the container. Inputs are typed, outputs are result objects rather than booleans or nullable users — so a caller branches on a status, not on a truthiness accident.
use Simtabi\Laranail\AuthKit\Contracts\AttemptEmailPasswordLoginInterface;
use Simtabi\Laranail\AuthKit\Contracts\LoginUserInterface;
use Simtabi\Laranail\AuthKit\Enums\AuthStatus;
public function store(
Request $request,
AttemptEmailPasswordLoginInterface $attempt,
LoginUserInterface $login,
) {
$result = $attempt->execute(request: $request, guard: 'web');
return match ($result->status) {
AuthStatus::Passed => tap(redirect()->intended(), fn () => $login->execute($result->user, 'web')),
AuthStatus::Failed => back()->withErrors(['email' => 'Invalid credentials.']),
AuthStatus::Throttled => abort(429),
};
}Credential verification and session issuance are separate actions on purpose. A flow that needs to check a password without logging anyone in — a re-authentication prompt, a token exchange — calls the first and not the second.
use Simtabi\Laranail\AuthKit\Contracts\IssueTokenForUserInterface;
$token = app(IssueTokenForUserInterface::class)->execute($user, name: 'mobile');
// $token->token — the plain-text token, shown onceEvery contract is bound in the container, so replacing one is a binding, not a fork:
$this->app->bind(
AttemptEmailPasswordLoginInterface::class,
MyCredentialCheck::class,
);- Configuration — the guard, rate limits, providers
- Architecture — why it is shaped this way, and the extension seams
- Login, registration, password reset — flow by flow