joe-404/laravel-auth
Config-driven, drop-in auth for Laravel 12/13: JSON API for registration with OTP/magic-link verification, login, refresh tokens, password reset, Google OAuth, multi-session/device fingerprinting, long-lived API tokens, account status workflows, and referrals.
Every key in config/auth_system.php — what it does, what values it accepts, which .env variable controls it, and the default. Copy-pasteable examples throughout.
Publish the config file so you can edit it locally:
php artisan vendor:publish --tag=auth-config
This page below documents the v2.6 config surface. The keys added across v2.6.1 → v2.7.3 are not yet integrated into the per-section deep-dives — they are listed here as a quick reference with the .env variable and default. Full descriptions live in the inline comments of config/auth_system.php and in the root UPGRADING.md / CHANGELOG.md.
api_tokens| Key | Env | Default | Since |
|---|---|---|---|
mode |
AUTH_API_TOKENS_MODE |
customer_auth |
v2.7.1 |
grantable_abilities |
— | ['read'] |
v2.7.1 |
strict_abilities |
AUTH_API_TOKENS_STRICT |
false |
v2.7.1 |
require_step_up |
AUTH_API_TOKENS_REQUIRE_STEP_UP |
false |
v2.7.1 |
require_step_up_for_revoke |
AUTH_API_TOKENS_REQUIRE_STEP_UP_FOR_REVOKE |
false |
v2.7.3 |
admin_require_step_up |
AUTH_API_TOKENS_ADMIN_REQUIRE_STEP_UP |
false |
v2.7.1 (POST) / v2.7.2 (PATCH+DELETE) |
admin_middleware |
AUTH_API_TOKENS_ADMIN_MIDDLEWARE |
null |
v2.7.1 |
max_ttl_days |
AUTH_API_TOKENS_MAX_TTL_DAYS |
null |
v2.7.1 |
account.status| Key | Env | Default | Since |
|---|---|---|---|
admin_middleware |
AUTH_ACCOUNT_STATUS_ADMIN_MIDDLEWARE |
null |
v2.7.1 |
admin_actions.enforce_role_hierarchy |
AUTH_ACCOUNT_STATUS_HIERARCHY |
false |
v2.7.1 |
admin_actions.allow_self_action |
AUTH_ACCOUNT_STATUS_ALLOW_SELF |
false |
v2.7.1 |
admin_actions.allow_equal_rank |
AUTH_ACCOUNT_STATUS_ALLOW_EQUAL |
false |
v2.7.1 |
admin_actions.role_ranks |
— | ['super-admin'=>100,'admin'=>50] |
v2.7.1 |
account.deletion| Key | Default | Since |
|---|---|---|
snapshot_strip_fields |
null (uses response.hidden_user_fields) |
v2.7.3 |
password_reset| Key | Env | Default | Since |
|---|---|---|---|
auto_login |
AUTH_PASSWORD_RESET_AUTO_LOGIN |
true |
v2.7.1 |
social| Key | Env | Default | Since |
|---|---|---|---|
enforce_state |
AUTH_SOCIAL_ENFORCE_STATE |
false |
v2.7.1 |
security| Key | Env | Default | Since |
|---|---|---|---|
profile |
AUTH_SECURITY_PROFILE |
null (relaxed|balanced|high) |
v2.7.1 |
lockout.scope |
AUTH_LOCKOUT_SCOPE |
email (ip|email_and_ip) |
v2.7.1 |
lockout.backoff |
AUTH_LOCKOUT_BACKOFF |
false |
v2.7.1 |
trusted_devices| Key | Env | Default | Since |
|---|---|---|---|
registration_device_level |
AUTH_TRUST_REG_DEVICE_LEVEL |
high |
v2.7.1 |
response| Key | Default | Since |
|---|---|---|
hidden_user_fields |
['password','remember_token'] |
v2.7.1 |
referral_code| Key | Env | Default | Since |
|---|---|---|---|
admin_ability |
AUTH_REFERRAL_ADMIN_ABILITY |
'super-admin|admin' |
v2.7.3 |
admin_middleware |
AUTH_REFERRAL_ADMIN_MIDDLEWARE |
null |
v2.7.3 |
AUTH_SECURITY_PROFILE=high flips on every hardening flag the library exposes (unless the corresponding env var is already set, in which case the env value always wins):
api_tokens.strict_abilities = trueapi_tokens.require_step_up = trueapi_tokens.require_step_up_for_revoke = true (v2.7.3)api_tokens.admin_require_step_up = truesocial.enforce_state = truesecurity.lockout.scope = email_and_ippassword_reset.auto_login = falseaccount.status.admin_actions.enforce_role_hierarchy = trueaccount.status.require_step_up = truetrusted_devices.registration_device_level = mediumtwo_factor.required = trueAUTH_SECURITY_PROFILE=balanced enables only strict_abilities, enforce_state, and lockout.scope=email_and_ip. relaxed (or unset) is a no-op.
modeEnv: AUTH_MODE | Default: both
Controls what credential type the server issues after a successful login.
| Value | Behaviour |
|---|---|
api |
Always returns a Bearer token. Best for pure API backends, mobile apps. |
web |
Always uses a Laravel session cookie. Best for server-rendered apps. |
both |
Auto-detects per request — see detection order below. |
Detection order for both mode (first match wins):
X-Client-Type: mobile header → Bearer token (mobile TTL)spa_token = true and no X-Client-Type → Bearer token (SPA TTL)AUTH_MODE=both
spa_tokenEnv: AUTH_SPA_TOKEN | Default: false
Only applies when AUTH_MODE=both.
false — browser SPA clients get a session cookie (recommended, most secure)true — browser SPA clients get a Bearer token instead (same as mobile clients)AUTH_SPA_TOKEN=false
require_email_verificationEnv: AUTH_REQUIRE_VERIFICATION | Default: true
true — users who have not verified their email address cannot log in; login returns HTTP 403false — users can log in immediately after registering without verifying their email; useful for internal tools or dev environmentsAUTH_REQUIRE_VERIFICATION=true
routesControls how and where the package mounts its HTTP routes.
routes.registerEnv: AUTH_ROUTES_REGISTER | Default: true
true — routes auto-mount under the configured prefix and middleware at bootfalse — the package does NOT register routes; you must include the route file yourselfWhen to use false: when you need the endpoints inside an existing versioned Route::prefix('api/v2') group with your own middleware ordering.
// routes/api.php (manual mount example)
Route::prefix('api/v1/auth')
->middleware(['api', 'throttle:api'])
->group(base_path('vendor/joe-404/laravel-auth/routes/auth.php'));
routes.prefixEnv: AUTH_ROUTES_PREFIX | Default: auth
The URL prefix for all package routes. With the default, routes are at /auth/login. To use versioned URLs set this to api/v1/auth and they become /api/v1/auth/login.
AUTH_ROUTES_PREFIX=auth # → /auth/login, /auth/register
AUTH_ROUTES_PREFIX=api/v1/auth # → /api/v1/auth/login, /api/v1/auth/register
routes.middlewareDefault: null (package picks automatically based on mode)
When null:
api mode → ['api']web / both mode → session + cookie + CSRF + ['api']Override completely by setting an array:
// config/auth_system.php
'routes' => [
'middleware' => ['api', 'my-custom-throttle'],
],
registrationOptions that extend what data users can submit during registration.
extra_fields_rulesDefault: []
A map of field_name => validation_rules. These fields are validated alongside email on POST /auth/register and are written to User::create() when registration is finalized.
Rules can be a pipe-separated string:
'extra_fields_rules' => [
'phone' => 'nullable|string|max:20',
'country' => 'required|string|size:2',
],
Or an array (required when using object rule classes):
'extra_fields_rules' => [
'username' => ['required', 'string', 'min:3', 'max:30', 'unique:users,username'],
'date_of_birth' => ['required', 'date', 'before:18 years ago'],
'agreed_terms' => ['required', 'accepted'],
'agreed_18_plus'=> ['required', 'accepted'],
],
Important: every field listed here must be in your User model's $fillable, otherwise User::create() silently ignores it.
extra_fields_messagesDefault: []
Custom error messages for extra field validation. Standard Laravel field.rule format.
'extra_fields_messages' => [
'username.required' => 'Please choose a username.',
'username.unique' => 'That username is already taken.',
'username.min' => 'Username must be at least 3 characters.',
'agreed_terms.accepted' => 'You must accept our Terms of Service to continue.',
'date_of_birth.before' => 'You must be at least 18 years old to register.',
],
Any key not listed here falls back to Laravel's built-in message.
extra_fields_transformersDefault: []
Derive or normalise a column value from the validated registration data — without writing a custom controller. The key is the target column name, the value is a class implementing ExtraFieldTransformerContract.
'extra_fields_transformers' => [
'username_normalized' => \App\Transformers\UsernameNormalizer::class,
],
The transformer runs after validation passes and before User::create(). The result is written to the target column. See docs/customization.md for the full contract and examples.
Security note: transformers cannot bypass the built-in privileged-field denylist. These target names are always stripped, even from transformer output: role, roles, is_admin, admin, email_verified_at, password, password_change_required.
request_classDefault: null
Override the built-in RegisterRequest with your own FormRequest subclass — for complex conditional rules, custom messages, or validation logic that can't be expressed as rule strings.
'registration' => [
'request_class' => \App\Http\Requests\MyRegisterRequest::class,
],
request_class takes priority over extra_fields_rules when both are set. See docs/customization.md for the subclassing example.
referral_codeEnv: multiple | Default: all off
When enabled, generates a unique referral code per user during registration and writes it to the configured column.
'referral_code' => [
'enabled' => env('AUTH_REFERRAL_CODE_ENABLED', false), // master switch
'column' => env('AUTH_REFERRAL_CODE_COLUMN', 'referral_code'),
'length' => env('AUTH_REFERRAL_CODE_LENGTH', 10),
'uppercase' => env('AUTH_REFERRAL_CODE_UPPERCASE', true),
'generator' => env('AUTH_REFERRAL_CODE_GENERATOR', null), // FQCN or null
],
| Key | Effect |
|---|---|
enabled |
false (default) = nothing happens. true = generate a code for every new user. |
column |
The users table column that stores the code. Must be in $fillable and your migration. |
length |
Number of characters in the generated code. Default: 10. |
uppercase |
true = code is all uppercase (default). false = mixed case. |
generator |
FQCN of a class implementing ReferralCodeGeneratorContract. Leave null to use the default random alphanumeric generator. |
Will not overwrite: if the user already supplied a value for the referral column via extra_fields_rules, the package will not overwrite it.
Required migration when enabling:
Schema::table('users', function (Blueprint $table): void {
$table->string('referral_code', 20)->nullable()->unique();
});
See docs/customization.md for the custom generator contract.
verificationControls how users verify their email address after registration.
'verification' => [
'method' => env('AUTH_VERIFICATION_METHOD', 'both'),
'otp_length' => env('AUTH_OTP_LENGTH', 6),
'otp_expiry' => env('AUTH_OTP_EXPIRY', 10),
'otp_max_attempts' => env('AUTH_OTP_MAX_ATTEMPTS', 5),
'magic_expiry' => env('AUTH_MAGIC_EXPIRY', 30),
'magic_link_target' => env('AUTH_MAGIC_LINK_TARGET', 'backend'),
'frontend_verify_url' => env('AUTH_FRONTEND_VERIFY_URL', null),
'frontend_reset_url' => env('AUTH_FRONTEND_RESET_URL', null),
],
| Key | Env | Default | Description |
|---|---|---|---|
method |
AUTH_VERIFICATION_METHOD |
both |
otp = numeric code only; magic_link = clickable link only; both = one email with OTP + link simultaneously |
otp_length |
AUTH_OTP_LENGTH |
6 |
Number of digits in the OTP code (4–8) |
otp_expiry |
AUTH_OTP_EXPIRY |
10 |
Minutes the OTP is valid before it expires |
otp_max_attempts |
AUTH_OTP_MAX_ATTEMPTS |
5 |
Wrong guesses before the OTP is invalidated (brute-force guard) |
magic_expiry |
AUTH_MAGIC_EXPIRY |
30 |
Minutes the magic link is valid |
magic_link_target |
AUTH_MAGIC_LINK_TARGET |
backend |
backend = link points to Laravel API; frontend = link points to your SPA/app, which then calls the API itself |
frontend_verify_url |
AUTH_FRONTEND_VERIFY_URL |
null |
Required when magic_link_target=frontend. Your SPA URL for email verification. The package appends ?token=xxx. |
frontend_reset_url |
AUTH_FRONTEND_RESET_URL |
null |
Required when magic_link_target=frontend. Your SPA URL for password reset. |
Frontend magic link flow (when magic_link_target=frontend):
Email link → https://myapp.com/verify-email?token=xxx
↓
SPA extracts token from URL
↓
SPA calls GET /auth/register/verify-magic/{token}
↓
API returns { completion_token: "..." }
password_resetControls how password reset codes or links are delivered.
'password_reset' => [
'method' => env('AUTH_PASSWORD_RESET_METHOD', null),
],
| Value | Effect |
|---|---|
null (default) |
Inherit from verification.method |
otp |
Send a numeric code only |
magic_link |
Send a clickable link only |
both |
Send one email with OTP + link |
Example: your app uses magic links for registration but you prefer OTP codes for the reset form (easier to type on mobile):
AUTH_VERIFICATION_METHOD=magic_link
AUTH_PASSWORD_RESET_METHOD=otp
passwordPassword policy enforced when users register or change their password.
'password' => [
'min_length' => env('AUTH_PASSWORD_MIN', 8),
'require_uppercase' => env('AUTH_PASSWORD_UPPERCASE', false),
'require_number' => env('AUTH_PASSWORD_NUMBER', false),
'require_special' => env('AUTH_PASSWORD_SPECIAL', false),
'pending_ttl_minutes' => env('AUTH_PENDING_TTL', 60),
],
| Key | Env | Default | Description |
|---|---|---|---|
min_length |
AUTH_PASSWORD_MIN |
8 |
Minimum number of characters |
require_uppercase |
AUTH_PASSWORD_UPPERCASE |
false |
Require at least one capital letter (A–Z) |
require_number |
AUTH_PASSWORD_NUMBER |
false |
Require at least one digit (0–9) |
require_special |
AUTH_PASSWORD_SPECIAL |
false |
Require at least one symbol (!@#$%...) |
pending_ttl_minutes |
AUTH_PENDING_TTL |
60 |
Minutes the pending registration is cached (between step 1 "initiate" and step 3 "complete"). If the user doesn't finish within this window, they must restart. |
Recommended production policy:
AUTH_PASSWORD_MIN=10
AUTH_PASSWORD_UPPERCASE=true
AUTH_PASSWORD_NUMBER=true
AUTH_PASSWORD_SPECIAL=true
token_ttlHow long access tokens and refresh tokens stay valid, broken out by client type.
'token_ttl' => [
'mobile' => [
'access_minutes' => env('AUTH_TOKEN_TTL_MOBILE', 10080), // 7 days
'refresh_minutes' => env('AUTH_REFRESH_TTL_MOBILE', 43200), // 30 days
],
'spa' => [
'access_minutes' => env('AUTH_TOKEN_TTL_SPA', 1440), // 24 hours
'refresh_minutes' => env('AUTH_REFRESH_TTL_SPA', 10080), // 7 days
],
'api' => [
'access_minutes' => env('AUTH_TOKEN_TTL_API', 525600), // 365 days
'refresh_minutes' => env('AUTH_REFRESH_TTL_API', 0), // 0 = never expires
],
'web' => [
'session_minutes' => env('AUTH_SESSION_TTL', 120), // keep in sync with SESSION_LIFETIME
],
],
| Client type | How it's detected |
|---|---|
mobile |
Login request has X-Client-Type: mobile header |
spa |
AUTH_MODE=both and AUTH_SPA_TOKEN=true |
api |
AUTH_MODE=api |
web |
AUTH_MODE=web or session mode in both |
Setting access_minutes or refresh_minutes to 0 means the token never expires (not recommended for short-lived clients).
rate_limitsRate limits applied per IP address and per email address independently. Exceeding either returns HTTP 429.
'rate_limits' => [
'register' => env('AUTH_RATE_REGISTER', '5:1'),
'login' => env('AUTH_RATE_LOGIN', '5:1'),
'otp_send' => env('AUTH_RATE_OTP_SEND', '3:1'),
'otp_verify' => env('AUTH_RATE_OTP_VERIFY', '10:5'),
'password_reset' => env('AUTH_RATE_PASSWORD_RESET', '3:1'),
],
Format: "max_attempts:decay_minutes" — e.g. "5:1" = 5 attempts per 1 minute.
| Key | Endpoint protected | Default |
|---|---|---|
register |
POST /auth/register |
5:1 |
login |
POST /auth/login |
5:1 |
otp_send |
POST /auth/email/resend-verification |
3:1 |
otp_verify |
POST /auth/register/verify-otp |
10:5 |
password_reset |
POST /auth/password/forgot |
3:1 |
Stricter production example:
AUTH_RATE_LOGIN=3:5
AUTH_RATE_PASSWORD_RESET=2:10
roles'roles' => [
'default_role' => env('AUTH_DEFAULT_ROLE', 'user'),
'seeded_roles' => ['super-admin', 'admin', 'user'],
],
| Key | Description |
|---|---|
default_role |
Role automatically assigned to every new user after they verify their email. The role must exist — run AuthRolesSeeder first. |
seeded_roles |
Roles that AuthRolesSeeder creates. Add any custom roles your app needs here. |
AUTH_DEFAULT_ROLE=member
To add a custom role:
// config/auth_system.php
'roles' => [
'default_role' => 'fan',
'seeded_roles' => ['super-admin', 'admin', 'fan', 'creator'],
],
Then re-run the seeder: php artisan db:seed --class="Joe404\LaravelAuth\Database\Seeders\AuthRolesSeeder"
otp_channelEnv: AUTH_OTP_CHANNEL | Default: email
Controls how OTP codes and magic links are delivered.
email — built-in email delivery (default)OtpChannelContract (SMS, WhatsApp, push notification, etc.)'otp_channel' => [
'driver' => env('AUTH_OTP_CHANNEL', 'email'),
// or:
'driver' => \App\Channels\SmsOtpChannel::class,
],
See docs/customization.md for the full contract and examples.
mailControls which notification classes are used for each email, and which account lifecycle emails are enabled.
Each key accepts null (use the built-in) or a FQCN of your own Notification class.
'mail' => [
// Registration / password reset emails
'otp_verify_notification' => null,
'otp_reset_notification' => null,
'magic_link_verify_notification' => null,
'magic_link_reset_notification' => null,
'otp_verify_combined_notification' => null,
'otp_reset_combined_notification' => null,
// Account lifecycle emails (v2.4)
'account_deleted_notification' => null,
'account_restored_notification' => null,
'account_purged_notification' => null,
'account_status_changed_notification' => null,
'account_deactivated_notification' => null,
'account_reactivated_notification' => null,
// Toggle which lifecycle emails are sent
'account_notifications_enabled' => [
'deleted' => true,
'restored' => true,
'purged' => false, // off by default (background worker action)
'status_changed' => false, // off by default (not always user-facing)
'deactivated' => true,
'reactivated' => true,
],
],
Custom notification constructor signature:
For OTP/magic-link notifications, your class constructor receives:
($code, $type, $context) for single-delivery($code, $url, $type, $context) for combinedAlternative: Blade view override (no PHP needed)
php artisan vendor:publish --tag=auth-views
Editable templates appear in resources/views/vendor/laravel-auth/emails/:
| File | Email sent for |
|---|---|
otp-verify.blade.php |
OTP code during registration |
otp-reset.blade.php |
OTP code for password reset |
magic-link-verify.blade.php |
Magic link during registration |
magic-link-reset.blade.php |
Magic link for password reset |
otp-verify-combined.blade.php |
OTP + link in one email (verification, method=both) |
otp-reset-combined.blade.php |
OTP + link in o... |
How can I help you explore Laravel packages today?