---
title: "Configuration — Auth for Laravel | Roundly"
description: "Every global and per-guard config key with its default — login methods, 2FA, passkeys, tokens, registration, throttles and more."
url: https://roundly-consulting.com/open-source/docs/auth-for-laravel/configuration
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)

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

```bash
php artisan vendor:publish --tag=authentication-config
```

```php
'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` | email | 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 second-factor policy only — never from a risk step-up nor from enrolling the passkey passkeys.mode = required mandates (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 — enrolment at every login without a passkey, email logins included (env AUTHENTICATION\_PASSKEYS). |
| `passkeys.second_factor` | allowed | off, allowed, required\_when\_enrolled or required (with passkeys.mode off only off or allowed). |
| `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; a route’s authentication.reauthenticated:N uses its own window instead. |
| `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 | 20 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; prefix and name are unique per guard (registration throws on a clash). |
| `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):

```bash
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 (a wrongly typed value too — it never crashes the doctor and hides the rest). The rules include:

- The model exists, is an Eloquent model and implements Account.
- 2FA on requires TwoFactorAuthenticatable; passkeys on (or passkey login) requires HasPasskeys.
- Passkeys off requires passkey login off and passkeys.second\_factor off or allowed (allowed only permits a passkey step, so AUTHENTICATION\_PASSKEYS=off works on the shipped defaults).
- 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. Distinct routes.prefix and routes.name are enforced when the routes register — a guard taking another guard’s prefix or name throws AuthenticationMisconfigured naming both.
- 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](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)
