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
| Key | Default | Env | Purpose |
|---|---|---|---|
default | users | AUTHENTICATION_GUARD | Guard used by Authentication::guard() without a name. |
key_type | bigint | AUTHENTICATION_KEY_TYPE | PK 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_key | derived from APP_KEY | AUTHENTICATION_HASH_KEY | HMAC key for links, codes, challenge tokens, fingerprints and throttle keys; rotating it invalidates every outstanding secret. |
tables.* | auth_* | AUTHENTICATION_*_TABLE | Table 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_store | default store | AUTHENTICATION_REAUTH_STORE | Cache 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.
| Key | Default | Purpose |
|---|---|---|
model | — (required) | The guard’s model: class-string<Model&Account>. |
laravel_guard | guard name | The auth.guards entry (a jwt guard) this guard authenticates with. |
identifier.columns | ['email'] | Columns accepted as the login identifier, tried in order. |
identifier.email_column | The address column — also where HasAuthentication routes mail (routeNotificationForMail()). | |
identifier.normalize | lowercase | lowercase or none; emails are stored and looked up normalised (always Unicode-composed, NFC). |
identifier.case_insensitive_lookup | false | lower(col) = ? for legacy mixed-case rows (index-hostile). |
login.password / .magic_link / .email_otp / .passkey | true / false / false / false | Enabled login methods (env AUTHENTICATION_LOGIN_*). |
login.reveal_account_state | true | Disabled/unverified codes after a verified first factor; false makes them invalid_credentials. |
challenge.ttl / .enrolment_ttl | 300 / 900 | Challenge lifetime in seconds (the longer one while an enrolment step is pending). |
challenge.max_attempts | 5 | Failed steps before the challenge dies (taken before a code is checked; a code that verifies gives its attempt back). |
challenge.allow_enrolment | true | Allow forced enrolment inside a challenge. |
challenge.enrolment_requires_verified_email | true | …only for verified addresses. |
challenge.bind.user_agent / .ip / .device_header | true / false / true | Device binding of a challenge. |
challenge.max_active_per_account | 3 | Older active challenges are superseded. |
two_factor.mode | optional | off, optional or required (env AUTHENTICATION_TWO_FACTOR). |
two_factor.after_email_login | true | Magic 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_passkey | false | A passkey primary still forces TOTP enrolment under required. |
two_factor.passkey_satisfies_required | true | A passkey second factor satisfies required. |
two_factor.issuer | null | otpauth issuer for this guard (null → two-factor’s issuer or the app name). |
two_factor.qr.enabled / .size | true / 240 | TOTP setup QR code. |
passkeys.mode | optional | off, optional or required (env AUTHENTICATION_PASSKEYS). |
passkeys.second_factor | allowed | off, allowed, required_when_enrolled or required. |
passkeys.satisfies_mfa | true | A 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_ttl | null | Seconds; null → jwt.ttl. |
tokens.refresh_ttl / .refresh_absolute_ttl | 30 / 90 days | Sliding and absolute refresh lifetime (0 = no cap). |
tokens.claims_resolver | DefaultClaimsResolver | ResolvesAccessTokenClaims implementation. |
tokens.include_email | true | Adds the email and email_verified claims. |
sessions.max_active | null | Oldest sessions are revoked above the cap. |
invalidation.* | others / all / others / others / none | password_changed, password_reset, email_changed, two_factor_changed, passkey_changed → none, others or all (disable, logout everywhere and incidents are always all). |
registration.mode | closed | open, invite_only or closed (env AUTHENTICATION_REGISTRATION); closed also refuses accepting invitations — use invite_only for invitation-only sign-up. |
registration.require_password | true | Require a password when password login is on. |
registration.login_after | true | Sign in right after registering. |
registration.rules | null | ProvidesRegistrationRules for host fields (only those keys reach the creator). |
registration.creator | CreateAccount | CreatesAccounts implementation. |
invitations.enabled | false | Invitations (invite_only requires it); while off, every invitations() call throws LoginMethodDisabled (404 method_disabled). |
invitations.ttl | 7 days | Invitation lifetime. |
invitations.lock_email | true | The invitee must use the invited address. |
invitations.replace_pending | true | A new invitation revokes the pending one for the address. |
invitations.allow_existing_email | false | Invite addresses that already have an account. |
invitations.resend_cooldown / .max_sends | 60 / 5 | Resend limits — they count mailed sends only (send: false and link() do not count). |
invitations.preview_payload_keys | [] | Payload keys the preview endpoint shows. |
invitations.ability | authentication.invitations.manage | Gate ability for the management routes. |
verification.mode | optional | off, optional, required_for_actions or required_for_login (env AUTHENTICATION_VERIFICATION). |
verification.channel | link | link or code. |
verification.ttl / .code_length / .max_attempts | 1 day / 6 / 5 | Verification secrets. |
verification.resend_decay | 60 | Per-account resend cooldown in seconds. |
verification.verify_on_email_login | true | A magic-link or email-code login verifies the address. |
email_change.enabled / .ttl / .notify_old / .require_reauthentication | true / 1 h / true / true | Verified email change (require_reauthentication = false drops its gate; otherwise required_for decides). |
magic_link.ttl / .same_device | 15 min / false | Magic links (same-device binds to the requesting device). |
email_otp.ttl / .length / .max_attempts | 10 min / 6 / 5 | Email codes (also re-authentication codes). |
passwords.reset.enabled / .ttl / .login_after | true / 1 h / false | Password reset. |
passwords.change.enabled | true | Change-password endpoint. |
passwords.rehash_on_login | true | Upgrade hashes on login. |
passwords.policy.min / .max | 10 / 128 | Length (72 bytes max under bcrypt, enforced). |
passwords.policy.letters / .mixed_case / .numbers / .symbols | false | Composition rules. |
passwords.policy.not_identifier | true | Must not contain the email’s local part. |
passwords.policy.uncompromised.* | false / 0 / 3 / false | Breached-password check: enabled, threshold, timeout, fail_closed (env AUTHENTICATION_PASSWORD_BREACH_CHECK). |
throttle.<kind>.max / .decay | see config | Ten rate-limit buckets — see Activity & risk. |
lockout.enabled / .threshold / .duration / .reset_unlocks | false / 10 / 900 / true | Opt-in hard lock. |
reauthentication.timeout | 900 | How long a re-authentication counts, in seconds. |
reauthentication.methods | all five | Allowed methods. |
reauthentication.require_second_factor_when_enrolled | true | Accounts 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_counts | true | A 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_for | all eight actions | SensitiveAction values that need a recent re-authentication. |
activity.enabled / .store_identifier / .retention_days | true / plain / 90 | Login-activity log (plain, hash or none). |
activity.new_device.enabled / .header / .skip_first_login | true / X-Device-Id / true | New-device detection. |
risk.assessor / .reactions.elevated / .reactions.high / .deny_response | null / notify / require_second_factor / uniform | Risk 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 / .timezone | X-Locale / [app.locale] / true / true / true | Locale and timezone. |
notifications.delivery / .connection / .queue | after_response / null / null | sync, after_response or queue (env AUTHENTICATION_NOTIFICATION_*). |
notifications.classes.* | packaged classes | 18 notification classes; null disables one. |
notifications.frontend_url | app.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 / .name | false / {guard}/auth / authentication.{guard}. | Opt-in routes. |
routes.middleware / .authenticated_middleware / .invitations_management | ['api'] / [] / false | Route middleware and the invitation admin routes. |
resources.account | AccountResource | The 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.comValidation
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 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.