Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Auth Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Customization Guide

Everything you can override in joe-404/laravel-auth without touching the package source.


Table of Contents

  1. Extra Registration Fields
  2. Custom Register Request (FormRequest subclass)
  3. Extra-field Validation Messages
  4. Extra-field Transformers
  5. Referral Codes
  6. Custom Response Formatter
  7. Custom OTP Channel (SMS, WhatsApp…)
  8. Custom Email Templates
  9. Custom Response Messages
  10. Multi-language Support
  11. Custom Referral Code Generator
  12. Custom Phone Driver (v2.6)
  13. All Contracts (quick reference)

1. Extra Registration Fields

Add any field to the registration form — no controller changes required.

How it works

Fields declared in extra_fields_rules are validated on POST /auth/register. Their validated values are held in cache and written to User::create() during POST /auth/register/complete (step 3).

Applies to social sign-in too (v2.6). The same extra_fields_rules drive the OAuth path when social.profile_completion.enabled is true: a brand-new Google user is sent to POST /auth/social/complete, which validates these exact rules before creating the account. Required fields block; optional fields are validated only if submitted. So you declare your fields once and both registration paths enforce them. See docs/configuration.mdsocial and the v2.6 section of docs/upgrading.md.

Simple example

// config/auth_system.php
'registration' => [
    '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',
        'phone'          => 'nullable|string|max:20',
    ],
],

Rules can be a pipe-separated string (as above) or an array (required for object-based rules):

'extra_fields_rules' => [
    'username' => ['required', 'string', 'min:3', Rule::unique('users', 'username')],
],

You must also:

  1. Add the field to User model's $fillable:
protected $fillable = [
    'name', 'email', 'password',
    'username', 'date_of_birth',  // ← add your fields here
];
  1. Add a migration column:
$table->string('username')->nullable()->unique();
$table->date('date_of_birth')->nullable();

Fields not in $fillable are silently ignored by User::create().


2. Custom Register Request

For complex conditional rules, or when extra_fields_rules is not flexible enough, extend the package's RegisterRequest:

// app/Http/Requests/MyRegisterRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Validation\Rule;
use Joe404\LaravelAuth\Http\Requests\RegisterRequest;

class MyRegisterRequest extends RegisterRequest
{
    public function rules(): array
    {
        return array_merge(parent::rules(), [
            'username' => ['required', 'string', 'min:3', Rule::unique('users')],
            'phone'    => ['required_if:country,LB', 'string', 'max:20'],
        ]);
    }

    public function messages(): array
    {
        return array_merge(parent::messages(), [
            'username.unique'           => 'That username is already taken.',
            'phone.required_if'         => 'Phone number is required for users in Lebanon.',
        ]);
    }
}

Wire it in config:

'registration' => [
    'request_class' => \App\Http\Requests\MyRegisterRequest::class,
],

request_class takes priority over extra_fields_rules — if both are set, the custom request class is used and extra_fields_rules is ignored.


3. Extra-field Validation Messages

Override validation error messages per field/rule without writing a custom request class.

// config/auth_system.php
'registration' => [
    'extra_fields_rules' => [
        'username'       => 'required|string|min:3|alpha_dash',
        'date_of_birth'  => 'required|date|before:18 years ago',
        'agreed_terms'   => 'required|accepted',
        'agreed_18_plus' => 'required|accepted',
    ],
    'extra_fields_messages' => [
        'username.required'      => 'Please choose a username.',
        'username.min'           => 'Username must be at least 3 characters.',
        'username.alpha_dash'    => 'Usernames may only contain letters, numbers, dashes, and underscores.',
        'date_of_birth.required' => 'Please enter your date of birth.',
        'date_of_birth.before'   => 'You must be at least 18 years old to register.',
        'agreed_terms.accepted'  => 'You must accept our Terms of Service to continue.',
        'agreed_18_plus.accepted'=> 'You must confirm that you are 18 or older.',
    ],
],

Format: "field.rule" => "message". Any key not listed falls back to Laravel's built-in message.

Error response shape (HTTP 422):

{
  "success": false,
  "message": "The given data was invalid.",
  "errors": {
    "username": ["Please choose a username."],
    "agreed_terms": ["You must accept our Terms of Service to continue."]
  }
}

4. Extra-field Transformers

Derive or normalise a column value from the validated registration data — without writing a custom controller.

Contract:

namespace Joe404\LaravelAuth\Contracts;

interface ExtraFieldTransformerContract
{
    public function transform(array $validated): mixed;
}

$validated contains all validated input: email plus every key from extra_fields_rules.

Step 1 — Create the transformer class:

// app/Transformers/UsernameNormalizer.php
<?php

namespace App\Transformers;

use Joe404\LaravelAuth\Contracts\ExtraFieldTransformerContract;

final class UsernameNormalizer implements ExtraFieldTransformerContract
{
    public function transform(array $validated): mixed
    {
        return strtolower(trim((string) ($validated['username'] ?? '')));
    }
}

Step 2 — Register it in config:

The array key is the target column name where the result is written.

'registration' => [
    'extra_fields_transformers' => [
        'username_normalized' => \App\Transformers\UsernameNormalizer::class,
    ],
],

Step 3 — Add the target column to your migration and $fillable:

// Migration
$table->string('username_normalized')->nullable()->unique();

// User.php
protected $fillable = [..., 'username_normalized'];

Another example — derive a display_name from first_name + last_name:

final class DisplayNameTransformer implements ExtraFieldTransformerContract
{
    public function transform(array $validated): mixed
    {
        $first = trim($validated['first_name'] ?? '');
        $last  = trim($validated['last_name'] ?? '');
        return trim("{$first} {$last}") ?: null;
    }
}
'extra_fields_transformers' => [
    'display_name' => \App\Transformers\DisplayNameTransformer::class,
],

Security: transformers cannot bypass the privileged-field denylist. These target names are always stripped even if a transformer writes to them: role, roles, is_admin, admin, email_verified_at, password, password_change_required.


5. Referral Codes

Generate a unique referral code for every new user during finalizeRegistration().

Looking for the full referral system — fingerprint anti-abuse, reward handlers, redeem endpoint, admin override, frontend integration guide? See docs/referral-codes.md for the complete walkthrough. This section only covers the generator side.

Enable in config

'referral_code' => [
    'enabled'   => true,
    'column'    => 'referral_code',
    'length'    => 8,
    'uppercase' => true,
    'generator' => null,   // null = built-in random alphanumeric
],

Required migration

Schema::table('users', function (Blueprint $table): void {
    $table->string('referral_code', 20)->nullable()->unique();
});

Add referral_code to User model's $fillable.

Will not overwrite

If the user already supplied a value for the referral column through extra_fields_rules, the package will not overwrite it.

Custom generator

See Custom Referral Code Generator below.


6. Custom Response Formatter

Swap the JSON envelope to match your API conventions.

The default envelope:

// success
{ "success": true, "message": "...", "data": {} }

// error
{ "success": false, "message": "...", "errors": {} }

Contract:

namespace Joe404\LaravelAuth\Contracts;

interface ResponseFormatterContract
{
    public function format(bool $success, string $message, array $data, array $errors): array;
}

Example — custom envelope:

// app/Auth/MyResponseFormatter.php
<?php

namespace App\Auth;

use Joe404\LaravelAuth\Contracts\ResponseFormatterContract;

final class MyResponseFormatter implements ResponseFormatterContract
{
    public function format(bool $success, string $message, array $data, array $errors): array
    {
        return [
            'ok'      => $success,
            'msg'     => $message,
            'payload' => $success ? $data : $errors,
        ];
    }
}

Register via config (recommended):

// config/auth_system.php
'response' => [
    'formatter' => \App\Auth\MyResponseFormatter::class,
],

Or via service container (config takes priority):

// app/Providers/AppServiceProvider.php
use Joe404\LaravelAuth\Contracts\ResponseFormatterContract;

public function register(): void
{
    $this->app->bind(ResponseFormatterContract::class, \App\Auth\MyResponseFormatter::class);
}

7. Custom OTP Channel

Replace the built-in email delivery with any channel — SMS, WhatsApp, push notification, etc.

Single-delivery contract

Use when your channel sends either the OTP code or the magic link, but not both at once:

namespace Joe404\LaravelAuth\Contracts;

interface OtpChannelContract
{
    public function send(string $recipient, string $code, string $type, array $context = []): void;
}
Parameter Description
$recipient Email address (or phone number if your channel uses phone)
$code The OTP digits (e.g. "482910") for type=otp; the full URL for type=magic_link
$type email_verify, magic_link_verify, password_reset, magic_link_reset
$context Extra metadata (user ID, locale, etc.)

Example — Twilio SMS:

// app/Channels/SmsOtpChannel.php
<?php

namespace App\Channels;

use Joe404\LaravelAuth\Contracts\OtpChannelContract;
use Twilio\Rest\Client;

final class SmsOtpChannel implements OtpChannelContract
{
    public function __construct(private readonly Client $twilio) {}

    public function send(string $recipient, string $code, string $type, array $context = []): void
    {
        // For magic link types, $code contains the full URL
        $message = str_contains($type, 'magic_link')
            ? "Click to verify: {$code}"
            : "Your code: {$code}";

        $this->twilio->messages->create($recipient, [
            'from' => config('services.twilio.from'),
            'body' => $message,
        ]);
    }
}

Combined-delivery contract

Use when your channel can send OTP code and magic link in a single message (e.g. a WhatsApp template that includes a button):

namespace Joe404\LaravelAuth\Contracts;

interface CombinedOtpChannelContract extends OtpChannelContract
{
    public function sendCombined(string $recipient, string $code, string $url, string $type, array $context = []): void;
}

When verification.method=both and your channel implements CombinedOtpChannelContract, the package calls sendCombined() instead of calling send() twice. This lets you send a single message that contains both the code and the link.

final class WhatsAppChannel implements CombinedOtpChannelContract
{
    public function send(string $recipient, string $code, string $type, array $context = []): void
    {
        // Fallback: code-only (called when type is not "both")
        WhatsApp::sendText($recipient, "Your code: {$code}");
    }

    public function sendCombined(string $recipient, string $code, string $url, string $type, array $context = []): void
    {
        // Single message with code + clickable link
        WhatsApp::sendTemplate($recipient, 'auth_verify', [
            'code' => $code,
            'url'  => $url,
        ]);
    }
}

Register in config:

'otp_channel' => [
    'driver' => \App\Channels\SmsOtpChannel::class,
],

8. Custom Email Templates

Two options — pick one per email, or mix.

Option A — Publish and edit Blade views (no PHP required)

php artisan vendor:publish --tag=auth-views

Edit files in resources/views/vendor/laravel-auth/emails/:

Template Email
otp-verify.blade.php OTP code for registration
otp-reset.blade.php OTP code for password reset
magic-link-verify.blade.php Magic link for registration
magic-link-reset.blade.php Magic link for password reset
otp-verify-combined.blade.php OTP + link for registration (method=both)
otp-reset-combined.blade.php OTP + link for password reset (method=both)

Variables available in templates: $code, $url (magic link only), $type, $user.

Option B — Custom Notification class (full control)

Point any mail.* config key to your own Notification class.

Constructor signatures:

  • Single delivery: __construct(string $code, string $type, array $context)
  • Combined delivery: __construct(string $code, string $url, string $type, array $context)
// app/Notifications/MyVerificationEmail.php
<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class MyVerificationEmail extends Notification
{
    public function __construct(
        private readonly string $code,
        private readonly string $type,
        private readonly array $context = [],
    ) {}

    public function via(mixed $notifiable): array
    {
        return ['mail'];
    }

    public function toMail(mixed $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Verify your email — Acme')
            ->line("Your code is: **{$this->code}**")
            ->line('This code expires in 10 minutes.');
    }
}
// config/auth_system.php
'mail' => [
    'otp_verify_notification' => \App\Notifications\MyVerificationEmail::class,
],

Option B takes priority over Option A for the same email slot. You can mix — override only the emails you care about and leave the rest using the built-in Blade template.

Account lifecycle emails (v2.4)

Same pattern for account lifecycle notifications:

Config key Sent when
account_deleted_notification User deletes their account
account_restored_notification Account auto-restored on login during grace
account_purged_notification Purge worker permanently anonymises the account
account_status_changed_notification Admin changes the user's status
account_deactivated_notification User deactivates their account
account_reactivated_notification Account auto-reactivated on login

Toggle individual emails without removing the class:

'mail' => [
    'account_notifications_enabled' => [
        'deleted'        => true,
        'restored'       => true,
        'purged'         => false,
        'status_changed' => false,
        'deactivated'    => true,
        'reactivated'    => true,
    ],
],

9. Custom Response Messages

Override any success or error message with a static string.

// config/auth_system.php
'messages' => [
    'register_initiated' => 'Almost there! Check your inbox for a verification code.',
    'register_complete'  => 'Welcome to Acme!',
    'login_success'      => 'Welcome back.',
    'logout_success'     => null,  // null = keep the built-in default
],

'errors' => [
    'invalid_credentials' => 'That email or password is incorrect.',
    'account_locked'      => 'Account locked. Try again in :seconds seconds.',
],

Setting a key to null or '' re-enables the translation pipeline for that key. See docs/localization.md for the full key list and multi-language support.


10. Multi-language Support

The package ships built-in English and Arabic translations. Every user-facing string goes through a three-step resolver:

  1. config('auth_system.messages.<key>') / config('auth_system.errors.<key>') — wins if non-null
  2. trans('auth_system::messages.<key>') / trans('auth_system::errors.<key>') — per-locale
  3. Built-in English fallback

Publish the language files:

php artisan vendor:publish --tag=auth-lang

Files appear at lang/vendor/auth_system/en/messages.php and errors.php.

Add a new locale (e.g. French):

lang/vendor/auth_system/fr/messages.php
lang/vendor/auth_system/fr/errors.php

Set locale per request:

// app/Http/Middleware/SetLocaleFromHeader.php
app()->setLocale($request->header('Accept-Language', 'en'));

See docs/localization.md for the complete guide, full key list, and placeholders.


11. Custom Referral Code Generator

Replace the built-in random alphanumeric generator with your own.

Contract:

namespace Joe404\LaravelAuth\Contracts;

interface ReferralCodeGeneratorContract
{
    public function generate(): string;
}

Example — word-list style codes:

// app/Auth/HumanReferralGenerator.php
<?php

namespace App\Auth;

use Illuminate\Support\Str;
use Joe404\LaravelAuth\Contracts\ReferralCodeGeneratorContract;

final class HumanReferralGenerator implements ReferralCodeGeneratorContract
{
    public function generate(): string
    {
        // e.g. "BRAVE-WHALE-7423"
        return strtoupper(Str::slug(fake()->words(2, true)) . '-' . rand(1000, 9999));
    }
}

Register in config:

'referral_code' => [
    'enabled'   => true,
    'generator' => \App\Auth\HumanReferralGenerator::class,
],

The package binds the generator to the container and resolves it via app()->make($fqcn), so it supports constructor injection.


12. Custom Phone Driver (v2.6)

The phone verification + SMS-2FA system delivers codes through a driver chosen per channel (sms, voice, whatsapp). Built-in drivers: log (dev), infobip, messagecentral, twilio, firebase. To use any other provider (Vonage, Plivo, AWS SNS, an on-prem SMS gateway…), write a driver and register it.

Step 1 — Implement the contract

<?php

namespace App\Phone;

use Joe404\LaravelAuth\Contracts\PhoneDriverContract;
use Joe404\LaravelAuth\Exceptions\PhoneVerificationException;
use Illuminate\Support\Facades\Http;

class VonageDriver implements PhoneDriverContract
{
    /** [@param](https://github.com/param) array<string,mixed> $config  The provider's config block. */
    public function __construct(private readonly array $config) {}

    public function send(string $phone, string $code, string $channel, array $context = []): void
    {
        $response = Http::asForm()->post('https://rest.nexmo.com/sms/json', [
            'api_key'    => $this->config['api_key'] ?? '',
            'api_secret' => $this->config['api_secret'] ?? '',
            'to'         => ltrim($phone, '+'),
            'from'       => $this->config['from'] ?? 'MyApp',
            'text'       => "Your verification code is: {$code}",
        ]);

        // Throw PhoneVerificationException on failure so the channel's
        // fallback provider (if configured) takes over.
        if (! $response->successful()) {
            throw new PhoneVerificationException(
                "Vonage send failed: HTTP {$response->status()}",
                'phone_send_failed',
            );
        }
    }

    /** Channels this driver can deliver. A channel not listed here triggers a config error. */
    public function supports(): array
    {
        return ['sms'];   // add 'voice' / 'whatsapp' only if you implement them
    }

    public function name(): string
    {
        return 'vonage';
    }
}

Step 2 — Register the provider in config

// config/auth_system.php → phone.providers
'vonage' => [
    'driver'     => \App\Phone\VonageDriver::class,
    'api_key'    => env('VONAGE_API_KEY'),
    'api_secret' => env('VONAGE_API_SECRET'),
    'from'       => env('VONAGE_FROM', 'MyApp'),
],

The whole provider block is passed to your driver's constructor as $config, so any keys you add are available as $this->config['…'].

Step 3 — Point a channel at it

// config/auth_system.php → phone.channels
'sms' => ['provider' => 'vonage', 'fallback' => 'log'],

That's it — no service-provider code needed. The package's PhoneDriverManager resolves the driver class from the container (so you can type-hint dependencies in the constructor alongside $config).

Alternative — register a closure at runtime

If you need to build the driver yourself (custom client, secrets manager, etc.), call PhoneDriverManager::extend() in your AppServiceProvider::boot():

use Joe404\LaravelAuth\Phone\PhoneDriverManager;

public function boot(): void
{
    $this->app->make(PhoneDriverManager::class)
        ->extend('vonage', function ($app, array $config) {
            return new \App\Phone\VonageDriver($config);
        });
}

A closure registered this way wins over the `dr...

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
terminal42/code-quality-tools
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky