NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Auth for Laravel

Configuration

Everything lives in config/authentication.php. Top-level keys are global. Everything under defaults is per guard and can be overridden in guards.<name> — associative arrays merge recursively, lists replace wholesale (identifier.columns = ['username'] replaces ['email'], it does not merge by index).

php artisan vendor:publish --tag=authentication-config
'guards' => [
    'users' => ['model' => App\Models\User::class],
    'clients' => [
        'model' => App\Models\Client::class,
        'login' => ['password' => false, 'magic_link' => true, 'passkey' => true],
        'two_factor' => ['mode' => 'off'],
        'passkeys' => ['mode' => 'optional', 'second_factor' => 'off'],
        'registration' => ['mode' => 'open'],
        'routes' => ['enabled' => true, 'prefix' => 'clients/auth'],
    ],
],

Every guard is isolated: own model and table, own JWT audience, own refresh-token owner type, own throttle keys, own activity rows, own routes. Two guards may not share a model (a role on one table is authorization, not a guard) or an audience.

Global keys

KeyDefaultEnvPurpose
defaultusersAUTHENTICATION_GUARDGuard used by Authentication::guard() without a name.
key_typebigintAUTHENTICATION_KEY_TYPEPK type of every guard model (bigint, uuid, ulid — blank keeps bigint, any other value throws an InvalidConfigurationException); must equal passkeys.key_type and refresh-tokens.key_type.
hash_keyderived from APP_KEYAUTHENTICATION_HASH_KEYHMAC key for links, codes, challenge tokens, fingerprints and throttle keys; rotating it invalidates every outstanding secret.
tables.*auth_*AUTHENTICATION_*_TABLETable names: challenges, one_time_tokens, invitations, login_activities.
models.*packaged models—Swappable models — the packaged class or a subclass of it; any other class throws an InvalidConfigurationException naming the key.
columns.*same-named—Column names on every guard table (token_version, locale, timezone, password, password_changed_at, email_verified_at, last_login_at, disabled_at, disabled_reason, locked_until, failed_login_count).
reauthentication_storedefault storeAUTHENTICATION_REAUTH_STORECache store for “recently re-authenticated” markers.

Per-guard keys (defaults.*)

Every on/off key accepts the usual env spellings — true/false, 1/0, on/off, yes/no — so AUTHENTICATION_LOGIN_PASSWORD=off really turns password login off. A key that is not set — absent, null or blank (empty or whitespace, what KEY= in .env gives) — reads as the default shown; anything else (a typo such as disabled) throws AuthenticationMisconfigured naming the key when the guard resolves, and authentication:check lists it. The two-way string keys (identifier.normalize, risk.deny_response) and the mode keys (two_factor.mode, registration.mode, notifications.delivery, …) are just as strict: blank takes the shipped default, a typo throws.

KeyDefaultPurpose
model— (required)The guard’s model: class-string<Model&Account>.
laravel_guardguard nameThe auth.guards entry (a jwt guard) this guard authenticates with.
identifier.columns['email']Columns accepted as the login identifier, tried in order.
identifier.email_columnemailThe address column — also where HasAuthentication routes mail (routeNotificationForMail()).
identifier.normalizelowercaselowercase or none; emails are stored and looked up normalised (always Unicode-composed, NFC).
identifier.case_insensitive_lookupfalselower(col) = ? for legacy mixed-case rows (index-hostile).
login.password / .magic_link / .email_otp / .passkeytrue / false / false / falseEnabled login methods (env AUTHENTICATION_LOGIN_*).
login.reveal_account_statetrueDisabled/unverified codes after a verified first factor; false makes them invalid_credentials.
challenge.ttl / .enrolment_ttl300 / 900Challenge lifetime in seconds (the longer one while an enrolment step is pending).
challenge.max_attempts5Failed steps before the challenge dies (taken before a code is checked; a code that verifies gives its attempt back).
challenge.allow_enrolmenttrueAllow forced enrolment inside a challenge.
challenge.enrolment_requires_verified_emailtrue…only for verified addresses.
challenge.bind.user_agent / .ip / .device_headertrue / false / trueDevice binding of a challenge.
challenge.max_active_per_account3Older active challenges are superseded.
two_factor.modeoptionaloff, optional or required (env AUTHENTICATION_TWO_FACTOR).
two_factor.after_email_logintrueMagic link, email code and invitation logins also need the second factor; false exempts them from the two-factor policy, never from a risk step-up (registration is never exempt).
two_factor.required_with_passkeyfalseA passkey primary still forces TOTP enrolment under required.
two_factor.passkey_satisfies_requiredtrueA passkey second factor satisfies required.
two_factor.issuernullotpauth issuer for this guard (null → two-factor’s issuer or the app name).
two_factor.qr.enabled / .sizetrue / 240TOTP setup QR code.
passkeys.modeoptionaloff, optional or required (env AUTHENTICATION_PASSKEYS).
passkeys.second_factorallowedoff, allowed, required_when_enrolled or required.
passkeys.satisfies_mfatrueA user-verified passwordless passkey login needs no further factor (user verification is then enforced per ceremony) — also not under a risk step-up; with false a step-up demands TOTP.
tokens.access_ttlnullSeconds; null → jwt.ttl.
tokens.refresh_ttl / .refresh_absolute_ttl30 / 90 daysSliding and absolute refresh lifetime (0 = no cap).
tokens.claims_resolverDefaultClaimsResolverResolvesAccessTokenClaims implementation.
tokens.include_emailtrueAdds the email and email_verified claims.
sessions.max_activenullOldest sessions are revoked above the cap.
invalidation.*others / all / others / others / nonepassword_changed, password_reset, email_changed, two_factor_changed, passkey_changed → none, others or all (disable, logout everywhere and incidents are always all).
registration.modeclosedopen, invite_only or closed (env AUTHENTICATION_REGISTRATION); closed also refuses accepting invitations — use invite_only for invitation-only sign-up.
registration.require_passwordtrueRequire a password when password login is on.
registration.login_aftertrueSign in right after registering.
registration.rulesnullProvidesRegistrationRules for host fields (only those keys reach the creator).
registration.creatorCreateAccountCreatesAccounts implementation.
invitations.enabledfalseInvitations (invite_only requires it); while off, every invitations() call throws LoginMethodDisabled (404 method_disabled).
invitations.ttl7 daysInvitation lifetime.
invitations.lock_emailtrueThe invitee must use the invited address.
invitations.replace_pendingtrueA new invitation revokes the pending one for the address.
invitations.allow_existing_emailfalseInvite addresses that already have an account.
invitations.resend_cooldown / .max_sends60 / 5Resend limits — they count mailed sends only (send: false and link() do not count).
invitations.preview_payload_keys[]Payload keys the preview endpoint shows.
invitations.abilityauthentication.invitations.manageGate ability for the management routes.
verification.modeoptionaloff, optional, required_for_actions or required_for_login (env AUTHENTICATION_VERIFICATION).
verification.channellinklink or code.
verification.ttl / .code_length / .max_attempts1 day / 6 / 5Verification secrets.
verification.resend_decay60Per-account resend cooldown in seconds.
verification.verify_on_email_logintrueA magic-link or email-code login verifies the address.
email_change.enabled / .ttl / .notify_old / .require_reauthenticationtrue / 1 h / true / trueVerified email change (require_reauthentication = false drops its gate; otherwise required_for decides).
magic_link.ttl / .same_device15 min / falseMagic links (same-device binds to the requesting device).
email_otp.ttl / .length / .max_attempts10 min / 6 / 5Email codes (also re-authentication codes).
passwords.reset.enabled / .ttl / .login_aftertrue / 1 h / falsePassword reset.
passwords.change.enabledtrueChange-password endpoint.
passwords.rehash_on_logintrueUpgrade hashes on login.
passwords.policy.min / .max10 / 128Length (72 bytes max under bcrypt, enforced).
passwords.policy.letters / .mixed_case / .numbers / .symbolsfalseComposition rules.
passwords.policy.not_identifiertrueMust not contain the email’s local part.
passwords.policy.uncompromised.*false / 0 / 3 / falseBreached-password check: enabled, threshold, timeout, fail_closed (env AUTHENTICATION_PASSWORD_BREACH_CHECK).
throttle.<kind>.max / .decaysee configTen rate-limit buckets — see Activity & risk.
lockout.enabled / .threshold / .duration / .reset_unlocksfalse / 10 / 900 / trueOpt-in hard lock.
reauthentication.timeout900How long a re-authentication counts, in seconds.
reauthentication.methodsall fiveAllowed methods.
reauthentication.require_second_factor_when_enrolledtrueAccounts with a second factor must use it — to re-authenticate, and for a re-authentication or fresh login to satisfy a gate (checked against the factors the account has now).
reauthentication.fresh_login_countstrueA login within the window counts — for an account with a second factor, only a login that used it (amr has mfa or hwk).
reauthentication.required_forall eight actionsSensitiveAction values that need a recent re-authentication.
activity.enabled / .store_identifier / .retention_daystrue / plain / 90Login-activity log (plain, hash or none).
activity.new_device.enabled / .header / .skip_first_logintrue / X-Device-Id / trueNew-device detection.
risk.assessor / .reactions.elevated / .reactions.high / .deny_responsenull / notify / require_second_factor / uniformRisk hooks (allow, notify, require_second_factor, deny); a step-up applies to every login method and denies an account with no factor to step up with.
locale.header / .supported / .store_on_registration / .fill_on_login / .timezoneX-Locale / [app.locale] / true / true / trueLocale and timezone.
notifications.delivery / .connection / .queueafter_response / null / nullsync, after_response or queue (env AUTHENTICATION_NOTIFICATION_*).
notifications.classes.*packaged classes18 notification classes; null disables one.
notifications.frontend_urlapp.url{frontend} in URL templates (env AUTHENTICATION_FRONTEND_URL).
notifications.urls.*{frontend}/auth/…?guard={guard}#token={token}Emailed link templates (env AUTHENTICATION_URL_*).
routes.enabled / .prefix / .namefalse / {guard}/auth / authentication.{guard}.Opt-in routes.
routes.middleware / .authenticated_middleware / .invitations_management['api'] / [] / falseRoute middleware and the invitation admin routes.
resources.accountAccountResourceThe me resource.

Strict reads

Every other key is read just as strictly, and checked when the guard resolves:

  • Integers (TTLs, attempt caps, throttles, lengths) take an int or a canonical integer string, so five or 1.5 throws instead of reading as the default (a blank one is not set, so the default applies) — and so does a value out of range: TTLs and caps at least 1, email_otp.length and verification.code_length 6–8, code attempts 1–100, passwords.policy.max at least policy.min.
  • 0 is accepted only where it means something: tokens.refresh_absolute_ttl (no cap), invitations.resend_cooldown, verification.resend_decay and passwords.policy.uncompromised.threshold. tokens.access_ttl and sessions.max_active are null or blank (jwt’s TTL / no cap) or at least 1.
  • String keys with a default (identifier.email_column, invitations.ability, activity.new_device.header, routes.prefix, routes.name, every notifications.urls.* template) throw when not a string, and read a blank as not set → the default; optional ones (laravel_guard, two_factor.issuer, queues, frontend_url, class-strings) throw when not a string, and read a blank as not set → unset.
  • List keys (identifier.columns, routes.middleware, locale.supported …) must be lists of non-empty strings — a bad entry throws, it is never dropped.

The global tables.* and columns.* names throw when not a string and read a blank as not set → the shipped name; default reads a blank as users; hash_key and reauthentication_store throw when not a string (blank is not set → derived key / default store).

Environment

The common switches are env-driven (on/off switches are parsed as booleans):

AUTHENTICATION_GUARD=users
AUTHENTICATION_KEY_TYPE=bigint
AUTHENTICATION_HASH_KEY=
AUTHENTICATION_USERS_MODEL=App\Models\User

# on/off switches accept true/false, 1/0, on/off, yes/no
AUTHENTICATION_LOGIN_PASSWORD=true
AUTHENTICATION_LOGIN_MAGIC_LINK=off
AUTHENTICATION_LOGIN_EMAIL_OTP=off
AUTHENTICATION_LOGIN_PASSKEY=off

AUTHENTICATION_TWO_FACTOR=optional
AUTHENTICATION_PASSKEYS=optional
AUTHENTICATION_REGISTRATION=closed
AUTHENTICATION_VERIFICATION=optional
AUTHENTICATION_PASSWORD_BREACH_CHECK=false

AUTHENTICATION_NOTIFICATION_DELIVERY=after_response
AUTHENTICATION_FRONTEND_URL=https://app.example.com

Validation

A guard is validated the first time it is resolved — the first problem is thrown as AuthenticationMisconfigured naming the key, and php artisan authentication:check lists them all. The rules include:

  • The model exists, is an Eloquent model and implements Account.
  • 2FA on requires TwoFactorAuthenticatable; passkeys on (or passkey login) requires HasPasskeys.
  • At least one login method is enabled; invite_only requires invitations.enabled.
  • Forced enrolment with enrolment_requires_verified_email requires verification.mode other than off.
  • laravel_guard is a jwt guard whose token_version is TokenVersionResolver.
  • authentication.key_type equals passkeys.key_type and refresh-tokens.key_type, and the model’s key matches.
  • With more than one guard: distinct laravel_guards, model morph classes and JWT audiences.
  • Every switch, enum, integer, string and list key reads cleanly (see Strict reads above), so a typo fails when the guard resolves — never mid-flow.

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

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