Login methods
Four primary methods, each switchable per guard. At least one must be enabled:
'guards' => [
'users' => [
'model' => App\Models\User::class,
'login' => [
'password' => true,
'magic_link' => true,
'email_otp' => true,
'passkey' => true, // passwordless
'reveal_account_state' => true, // disabled/unverified codes only after a verified first factor
],
'magic_link' => ['ttl' => 900, 'same_device' => false],
'email_otp' => ['ttl' => 600, 'length' => 6, 'max_attempts' => 5],
],
],$guard = Authentication::guard('users');
$context = $guard->contextFrom($request);
// Password
$result = $guard->attempt(new PasswordCredentials('[email protected]', $password), $context);
// Magic link: the request never reveals whether the address exists
$guard->requestMagicLink('[email protected]', $context);
$result = $guard->consumeMagicLink($token, $context);
// Email code
$guard->requestEmailOtp('[email protected]', $context);
$result = $guard->verifyEmailOtp('[email protected]', $code, $context);
// Passwordless passkey (discoverable credentials)
$options = $guard->passkeyLoginOptions($context); // for navigator.credentials.get()
$result = $guard->loginWithPasskey($assertion, $context); // passkeys' AuthenticationResponseDataPassword
The login, login_ip and login_account throttles are checked before any hashing. Unknown or passwordless accounts are checked against a dummy hash of the same cost, so response time does not reveal whether an account exists. A failure hits all three buckets, bumps the opt-in lockout counter and answers invalid_credentials.
Magic link
A real, enabled account receives a 64-character link with the secret in the URL fragment; the request always answers 202 sent. On consume, the link’s address must still be the account’s. With magic_link.same_device, the requesting device’s fingerprint is checked before the claim, so a mismatch does not burn the link.
Email code
Same request semantics as the magic link. Verification runs through the three login buckets (a code guess is a login attempt), never reveals attempts_left and binds the code to its address.
Passkey
Options are discoverable — no identifier, no allowCredentials. With passkeys.satisfies_mfa they require user verification, enforced for that ceremony. The ceremony id must come back with the assertion, another guard’s credential is refused, and every failure is a uniform invalid_credentials.
What every login goes through
- Possession side effects — a magic-link or email-code login verifies the address (verification.verify_on_email_login).
- Account state — disabled answers account_disabled (or invalid_credentials with reveal_account_state off); locked answers too_many_attempts; unverified under required_for_login answers email_not_verified.
- Password housekeeping — rehash, reset the lockout counter, clear the login bucket.
- Locale fill when the account has none, and new-device detection.
- Risk — the configured reaction can notify, require a second factor or deny, whatever the login method.
- Required steps — either a challenge, or the success tail: tokens, last_login_at, activity, LoginSucceeded and TokensIssued.
Authentication method references
Access tokens carry amr (RFC 8176). A completed challenge adds its factors and mfa:
| Primary method | amr |
|---|---|
| Password | pwd |
| Magic link | |
| Email code | otp |
| Passkey | hwk, user (+ mfa when user verification was required) |
| Invitation | email (none for an unverified chosen address) |
| Password reset | |
| Registration | pwd or none |
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.