The Authentication facade
Everything the HTTP layer does is available from PHP through RoundlyConsulting\Auth\Facades\Authentication (no Auth alias — it would collide with Laravel’s). Authentication::guard('clients') is one guard’s API:
use RoundlyConsulting\Auth\DataTransferObjects\PasswordCredentials;
use RoundlyConsulting\Auth\Facades\Authentication;
use RoundlyConsulting\Auth\Http\Resources\ChallengeResource;
use RoundlyConsulting\Auth\Http\Resources\TokenPairResource;
$guard = Authentication::guard('clients');
$result = $guard->attempt(
new PasswordCredentials(identifier: $request->string('email')->toString(), password: $request->string('password')->toString()),
$guard->contextFrom($request),
);
return $result->isAuthenticated()
? TokenPairResource::make($result->tokens)
: ChallengeResource::make($result->challenge); // continue with $guard->challenges()->complete(…)Every other per-guard method on the facade runs on the default guard (authentication.default), so Authentication::twoFactor()->status($user) is Authentication::guard()->twoFactor()->status($user):
use RoundlyConsulting\Auth\DataTransferObjects\InvitationData;
use RoundlyConsulting\Auth\Enums\InvalidationReason;
use RoundlyConsulting\Auth\Facades\Authentication;
// Every per-guard method runs on the default guard (authentication.default)…
Authentication::twoFactor()->status($user); // enabled, pending, recovery codes left, mode
Authentication::passwords()->set($user, 'n3w-Passphrase!', InvalidationReason::Security);
Authentication::invitations()->create(new InvitationData('[email protected]'))->url; // needs invitations.enabled
Authentication::lock($user, seconds: 3600);
// …or on a named guard
Authentication::guard('clients')->twoFactor()->status($client);
// Manager-level
Authentication::guards(); // ['users', 'clients']
Authentication::prune(days: 30); // PruneReport — what authentication:prune runsInvitations are off by default: until the guard sets invitations.enabled = true, every invitations() call throws LoginMethodDisabled (a 404 method_disabled over HTTP).
Core verbs
Authentication::guard($name) returns a GuardContext; each method runs one container-resolved action for that guard:
| Method | Returns |
|---|---|
attempt(PasswordCredentials, SessionContext) | LoginResult (tokens or challenge) |
requestMagicLink($email, $ctx) / consumeMagicLink($token, $ctx) | void / LoginResult |
requestEmailOtp($email, $ctx) / verifyEmailOtp($email, $code, $ctx) | void / LoginResult |
passkeyLoginOptions($ctx) / loginWithPasskey($response, $ctx) | RequestOptionsData / LoginResult |
register(RegistrationData) | RegistrationResult |
issueTokens($account, $ctx, $method, $amr) | TokenPair — host-vouched login (impersonation, SSO); fires TokensIssued |
refresh($refreshToken, $ctx) | TokenPair (the session’s previous access token stops working) |
sessions($account, $currentSessionId) | Collection<SessionData> |
logout($account, $current) / logoutSession($account, $id) / logoutOthers($account, $current) / logoutEverywhere($account) | — / — / int / int |
invalidate($account, InvalidationReason, ?$keep, ?$ctx) | ?TokenPair (the re-issued pair under others) |
disable($account, ?$reason) / enable($account) | — |
lock($account, ?$seconds) / unlock($account) | CarbonImmutable / — (a lock notifies the owner, never logs out) |
updateLocale($account, LocaleData) | Account |
activity($account, $perPage = 20) | Paginated own LoginActivity |
contextFrom(Request) / tokenFrom(Request) | SessionContext / CurrentToken |
name() / config() / accounts() | string / GuardConfig / AccountRepository |
Sub-contexts
Each area has its own sub-context — on a guard, or on the facade for the default guard ($a is an account):
| Sub-context | Methods |
|---|---|
twoFactor() | status($a), start($a), confirm($a, $code, ?$current, ?$ctx), disable($a, ?$current, ?$ctx), regenerateRecoveryCodes($a, ?$current, ?$ctx) |
passkeys() | all($a), registrationOptions($a), register($a, $response, ?$name, ?$current, ?$ctx), rename($a, Passkey|int, $name), remove($a, Passkey|int, ?$current, ?$ctx) |
passwords() | set($a, $password, $reason), change($a, ChangePasswordData), requestReset($email, $ctx), reset(PasswordResetData), validate($password, ?$a, ?$email), rule(?$email) |
email() | sendVerification($a, ?$ctx), requestVerification($a, $ctx), resendVerification($email, $ctx), verify($tokenOrCode, ?$email, $ctx), requestChange($a, EmailChangeData), confirmChange($token, $ctx) |
invitations() | create(InvitationData) → InvitationLink, accept(AcceptInvitationData) → RegistrationResult, paginate(?$status, $perPage), preview($token), find($id), resend(Invitation|int), revoke(Invitation|int), link(Invitation|int) |
reauthentication() | methods($a), sendCode($a, $ctx), passkeyOptions($a, $current), confirm($a, ReauthenticationData), ensureRecent($a, $current, ?$seconds), ensureFor(SensitiveAction, $a, $current) |
challenges() | complete(ChallengeFactorData), passkeyOptions($token, $ctx), passkeyEnrolmentOptions($token, $ctx), startTwoFactorEnrolment($token, $ctx) |
Plus Authentication::prune(?$days), guards(), routes($guard) and currentGuard().
Scoping
Every method that takes an account refuses one of another guard’s model (AuthenticationMisconfigured, “belongs to another guard”) before anything is written. Another guard’s invitation or challenge token, and another account’s passkey, are unknown (InvitationNotFound, ChallengeInvalid, PasskeyNotFound).
Acting for the signed-in user
Pass the caller’s token ($guard->tokenFrom($request)) as $current to the credential changes: its device is kept under others and the re-issued pair is returned — the client must swap to it. Leave $current out for admin and CLI calls. The HTTP layer also demands a recent re-authentication before the actions in reauthentication.required_for; PHP callers vouch for the user themselves, or run the same gate first:
use RoundlyConsulting\Auth\Enums\SensitiveAction;
use RoundlyConsulting\Auth\Facades\Authentication;
$guard = Authentication::guard('users');
$current = $guard->tokenFrom($request);
$guard->reauthentication()->ensureFor(SensitiveAction::DisableTwoFactor, $user, $current);
$tokens = $guard->twoFactor()->disable($user, $current, $guard->contextFrom($request)); // swap the client to $tokenspasswords()->change() runs the gate itself for an account that is setting its first password. Use Authentication::…, not the lower packages’ model methods: $user->disableTwoFactor(), $user->regenerateTwoFactorRecoveryCodes(), Passkeys::for($user)->revoke(), $user->revokeAllSessions() or RefreshTokens::sessions($user)->revokeAll() change the credential but skip this package’s policy — the guard’s mode, the invalidation, the events and the owner’s notification.
Request-derived inputs
$guard->contextFrom($request) captures the IP, user agent, the device id from the X-Device-Id header, the device_name and timezone inputs and the negotiated locale; construct a SessionContext directly for jobs and tests: new SessionContext(ipAddress: '203.0.113.9', userAgent: 'CLI'). $guard->tokenFrom($request) reads the caller’s access token (jti, sid, auth_time, amr).
Handling a login result
A LoginResult is either authenticated (a TokenPair) or requires a challenge (a PendingChallenge). Complete the remaining steps with challenges()->complete():
use RoundlyConsulting\Auth\DataTransferObjects\ChallengeFactorData;
use RoundlyConsulting\Auth\DataTransferObjects\PasswordCredentials;
use RoundlyConsulting\Auth\Enums\FactorMethod;
use RoundlyConsulting\Auth\Facades\Authentication;
$guard = Authentication::guard('users');
$context = $guard->contextFrom($request);
$result = $guard->attempt(new PasswordCredentials($request->input('identifier'), $request->input('password')), $context);
if ($result->requiresChallenge()) {
$pending = $result->challenge; // token, expiresAt, method, completed, remaining (ChallengeRequirement[]), attemptsLeft
// … later, with the user's TOTP code:
$result = $guard->challenges()->complete(new ChallengeFactorData(
challengeToken: $pending->token,
method: FactorMethod::Totp,
context: $context,
code: $request->input('code'),
));
}
$tokens = $result->tokens; // accessToken, accessExpiresAt, refreshToken, refreshExpiresAt, sessionId, accessTokenId, tokenTypeTo answer in the package’s JSON shape from your own controller, use LoginResponse:
use RoundlyConsulting\Auth\Http\Responses\LoginResponse;
// Render a LoginResult in the same shape as the package endpoints
return LoginResponse::make($result, $request);Host-driven sessions
issueTokens() mints a session for a login your code vouches for — impersonation, an SSO callback, tests — and fires TokensIssued:
use RoundlyConsulting\Auth\Enums\AuthMethodReference;
use RoundlyConsulting\Auth\Enums\InvalidationReason;
use RoundlyConsulting\Auth\Enums\LoginMethod;
// SSO callback: the host vouches for the authentication (fires TokensIssued)
$pair = Authentication::guard('users')->issueTokens($user, $context, LoginMethod::Host, [AuthMethodReference::Mfa]);
// Incident response
Authentication::guard('users')->invalidate($user, InvalidationReason::Security);
Authentication::guard('users')->disable($user, 'chargeback fraud');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.