bmatovu/multi-auth
Laravel MultiAuth simplifies authenticating multiple user types—such as admins and customers—with separate guards, providers, and tables. Configure guards, protect routes with middleware, and manage independent logins for each user model without conflicts.
bmatovu/multi-auth is a scaffolding package for running several independent Laravel auth guards (e.g. web for customers, admin for staff, vendor for partners) without hand-wiring every controller, route, view, and provider. Treat it as a generator you run once per guard, not a runtime library you call into constantly.
Minimal path from zero to working second guard:
composer require bmatovu/multi-auth
php artisan vendor:publish --tag=multi-auth
config/multi-auth.php first — it is the source of truth for the guard name, model namespace, route prefix, redirect targets, and view namespace the generator will emit.php artisan multi-auth:make admin
This typically scaffolds the Eloquent model, migration, guard/provider entries in config/auth.php, login/register controllers, routes, and Blade views.php artisan migrate and confirm config/auth.php now has a matching guards.admin and providers.admins pair pointing at the generated model.app/Http/Kernel.php (or bootstrap/app.php on Laravel 11+, which is officially supported as of v12.1.0) so auth:admin and guest:admin resolve.Version note: v12.1.0 adds first-class Laravel 11 support. If you are on Laravel 11 and previously had to hand-patch around middleware registration, that workaround can now be dropped — pin
"bmatovu/multi-auth": "^12.1"and use thebootstrap/app.phpmiddleware registration natively.
First real use case: split an existing single-guard app so /admin requires an Admin model while the public site keeps using User. Start by generating admin, then move only the admin routes into a group with middleware(['web', 'auth:admin']) — do not migrate the public guard at the same time, or you will spend the afternoon debugging session collisions.
Where to look when something is off, in order: the published config, the generated config/auth.php block, the generated controllers under app/Http/Controllers/Auth, then the package's own src/ commands if the output does not match your expectations.
One guard per actor type, one provider per model. Keep the naming symmetric (admin guard → admins provider → App\Models\Admin) and let the generator handle the boilerplate. Asymmetry here is the most common source of "wrong user logged in" bugs.
Route grouping by guard. Isolate each surface with its own middleware stack:
Route::middleware(['web', 'auth:admin'])->prefix('admin')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index']);
});
Use guest:admin on the login/register routes so an authenticated admin is never shown the login form.
Laravel 11+ middleware registration. With v12.1.0 the guard middleware can be registered the Laravel 11 way in bootstrap/app.php instead of editing Kernel.php:
->withMiddleware(function (Middleware $middleware) {
// No package-specific registration needed for the built-in
// auth / guest middleware aliases; just reference auth:admin
// and guest:admin directly in route groups.
})
If you were previously patching the package for Laravel 11, remove that patch — reinstall ^12.1 cleanly.
Explicit guard resolution everywhere. Never rely on the default guard when multiple exist. In controllers, policies, and Blade:
Auth::guard('admin')->user();
Auth::guard('admin')->check();
Auth::guard('admin')->logout();
In Blade, prefer @auth('admin') ... @endauth over @auth, which silently falls back to the default guard.
Redirect targets belong in config, not controllers. Point the generated LoginController's $redirectTo at a config value (or route('admin.dashboard')) so staging and production can diverge. Same for RedirectIfAuthenticated — the package's default redirect will send everyone to /home, which is usually wrong for a secondary guard.
Cross-guard access is a deliberate pattern, not the default. When an admin needs to act on a customer record, load the customer via its own model (User::find($id)) rather than trying Auth::guard('web'). The logged-in admin has no relationship to the customer's session.
Middleware composition for shared areas. For pages both a customer and a vendor can see, either stack auth:web,vendor (Laravel accepts multiple guards and passes if any matches) or write a thin EnsureAnyGuard middleware that records which guard succeeded and exposes it to the view.
Testing pattern. Always pin the guard in tests:
$this->actingAs($admin, 'admin')->get('/admin/dashboard')->assertOk();
$this->get('/admin/dashboard')->assertRedirect('/admin/login');
The second assertion catches the classic failure where auth:admin is missing from the route group but the test still passes because the default web guard is authenticated.
Password resets per guard. Each guard needs its own password broker entry in config/auth.php (passwords.admins with its own table and expiry). The generator emits this; verify it, because a missing broker means Password::broker('admins') silently fails at runtime.
'cookie' values on each session guard in config/auth.php if you see users bouncing between logins.auth:admin throws before the controller runs. A Route [login] not defined error on an unauthenticated hit usually means the guard's redirectTo points at a named route the generator did not create, or the route is defined with a different name (admin.login vs login). Grep for the name in config/auth.php and the generated controllers.php artisan multi-auth:make writes to config/auth.php at disk level. If config:cache was run beforehand, your new guard is invisible until php artisan config:clear. Make this the first step of any "it worked yesterday" debugging.Authenticatable. Generated models do, but if you reuse an existing model (e.g. Admin that already extends something else), you must implements Authenticatable and pull in AuthenticatableTrait, plus a getAuthIdentifierName that matches the provider's model key.email is global across the table. If two guards ever point at the same table (happens when copying config blocks), credentials leak between actor types. Audit config/auth.php after each generation for duplicated model entries.migrate, especially in CI where ordering is deterministic.vendor:publish --tag=multi-auth-views) and edit the published copies rather than the vendor tree — package updates will overwrite vendor files. Same for controllers: extend the generated controller in app/Http/Controllers rather than editing package classes.Illuminate\Auth\Events\Login fires for every guard. Use $event->guard inside listeners to branch, or you will trigger admin-only side effects on customer logins.^3.0 because you needed Laravel 11 support, upgrade to ^12.1 and re-run generation for any guards where you hand-patched middleware registration. The generated bootstrap/app.php wiring is now native. Re-check published views/controllers — a major version hop may have changed the generated output.composer.json ("bmatovu/multi-auth": "^12.1" for the current Laravel 11-compatible line) and budget a half-day for the first upgrade to skim what the generator now emits differently.How can I help you explore Laravel packages today?