---
title: "The Authentication facade — Auth for Laravel | Roundly"
description: "Tour the Authentication facade — per-guard login and session verbs, seven area sub-contexts, scoping rules and login results."
url: https://roundly-consulting.com/open-source/docs/auth-for-laravel/programmatic-api
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Auth for Laravel](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)

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

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

```php
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('ada@example.com'))->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 runs
```

Invitations 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, ?$actor) / enable($account, ?$actor)` | — (actor goes on AccountDisabled / AccountEnabled) |
| `lock($account, ?$seconds, ?$actor) / unlock($account, ?$actor)` | CarbonImmutable / — (a lock notifies the owner — with an actor, “locked by an administrator” — and 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, ?$actor), 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, ?$actor)` |
| `passwords()` | `set($a, $password, $reason, ?$actor), 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:

```php
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 $tokens
```

passwords()->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.

## Acting as an admin

passwords()->set(), twoFactor()->disable(), passkeys()->remove() and lock() take an optional, trailing actor: — pass it by name. With another actor, the owner’s mail says an administrator acted (see Notifications) and the event carries it as actor; disable(), enable() and unlock() take the same argument for their events only, since they send no mail:

```php
use RoundlyConsulting\Auth\Enums\InvalidationReason;
use RoundlyConsulting\Auth\Facades\Authentication;

// Support desk: pass the admin, so the owner is told an administrator acted
Authentication::passwords()->set($user, $temporaryPassword, InvalidationReason::Security, actor: $admin);
Authentication::lock($user, seconds: 3600, actor: $admin);
Authentication::twoFactor()->disable($user, actor: $admin);   // no $current: an admin keeps no device
Authentication::disable($user, 'chargeback fraud', actor: $admin);
```

The actor is informational: any saved model — an admin of another guard, a model that is no account — with no guard or ownership check. The account itself counts as no actor, so actor: $request->user() is always safe. An unsaved model, or another actor together with a $current token, throws InvalidArgumentException before anything is written — that token is the account’s own device, never the admin’s. Without actor: nothing changes.

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

```php
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, tokenType
```

To answer in the package’s JSON shape from your own controller, use LoginResponse:

```php
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:

```php
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', 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)
