Kritim Yantra
Dec 20, 2025
I’ve been there. You build a nice Laravel app, push it live, and suddenly you realize authentication isn’t just “login/register”… it’s also who can access what.
A proper multi-auth setup (Admin + User) saves you from:
if($user->is_admin) everywhere)Today we’ll build a clean Laravel 2026 multi-auth system with:
✅ Separate Admin and User login
✅ Separate guards (web and admin)
✅ Separate middleware (auth vs auth:admin)
✅ Admin & User profile pages
✅ Clean routes: /admin/... and /user/...
Here’s the flow:
Visitor
├── /register -> creates User
├── /login -> logs in User (guard: web)
└── /admin/login -> logs in Admin (guard: admin)
User session -> can access /user/dashboard, /user/profile
Admin session -> can access /admin/dashboard, /admin/profile
Think of it like two different keys 🔑:
composer create-project laravel/laravel multi-auth-2026
cd multi-auth-2026
Set your .env database:
DB_DATABASE=multi_auth
DB_USERNAME=root
DB_PASSWORD=
Run migrations:
php artisan migrate
At this point, you have a default users table.
If you want ready-made User login/register pages, Laravel Breeze is a simple option:
composer require laravel/breeze --dev
php artisan breeze:install
php artisan migrate
npm install
npm run dev
Now you have:
/register/login/profile (depending on stack)web)Pro Tip: Breeze keeps things clean. It’s great for beginners because you’re not reinventing the wheel.
Now we’ll create a completely separate admin system.
php artisan make:model Admin -m
Edit the generated migration in database/migrations/...create_admins_table.php:
public function up(): void
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
Run migration:
php artisan migrate
Now you have users and admins tables.
Open config/auth.php.
Find 'guards' and update like this:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
],
Find 'providers':
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
],
Now Laravel understands there are two types of accounts.
We’ll create a controller for admin auth:
php artisan make:controller Admin/AuthController
Open routes/web.php:
use App\Http\Controllers\Admin\AuthController as AdminAuthController;
use App\Http\Controllers\Admin\AdminController;
Route::prefix('admin')->name('admin.')->group(function () {
Route::get('/login', [AdminAuthController::class, 'showLogin'])
->middleware('guest:admin')
->name('login');
Route::post('/login', [AdminAuthController::class, 'login'])
->middleware('guest:admin')
->name('login.submit');
Route::post('/logout', [AdminAuthController::class, 'logout'])
->middleware('auth:admin')
->name('logout');
Route::get('/dashboard', [AdminController::class, 'dashboard'])
->middleware('auth:admin')
->name('dashboard');
});
Open app/Http/Controllers/Admin/AuthController.php:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function showLogin()
{
return view('admin.auth.login');
}
public function login(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::guard('admin')->attempt(
$request->only('email', 'password'),
$request->boolean('remember')
)) {
$request->session()->regenerate();
return redirect()->route('admin.dashboard');
}
return back()->withErrors([
'email' => 'Invalid admin credentials.',
])->onlyInput('email');
}
public function logout(Request $request)
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login');
}
}
Warning ️: Don’t use Auth::attempt() for admin. Always use Auth::guard('admin')->attempt() — otherwise Laravel logs you into the user guard and you’ll get super confusing behavior.
php artisan make:controller Admin/AdminController
app/Http/Controllers/Admin/AdminController.php:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
class AdminController extends Controller
{
public function dashboard()
{
return view('admin.dashboard');
}
}
Create folders:
resources/views/admin/auth/resources/views/admin/resources/views/admin/auth/login.blade.php:
<!doctype html>
<html>
<head>
<title>Admin Login</title>
</head>
<body>
<h2>Admin Login</h2>
@if ($errors->any())
<div style="color:red;">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('admin.login.submit') }}">
@csrf
<div>
<label>Email</label>
<input type="email" name="email" value="{{ old('email') }}" required>
</div>
<div>
<label>Password</label>
<input type="password" name="password" required>
</div>
<div>
<label>
<input type="checkbox" name="remember">
Remember Me
</label>
</div>
<button type="submit">Login</button>
</form>
</body>
</html>
resources/views/admin/dashboard.blade.php:
<!doctype html>
<html>
<head>
<title>Admin Dashboard</title>
</head>
<body>
<h2>Welcome Admin 🎉</h2>
<p>You are logged in as: <b>{{ auth('admin')->user()->email }}</b></p>
<form method="POST" action="{{ route('admin.logout') }}">
@csrf
<button type="submit">Logout</button>
</form>
</body>
</html>
Now let’s build /admin/profile similar to user profile, but for admins.
php artisan make:controller Admin/AdminProfileController
Add routes (inside the admin group):
use App\Http\Controllers\Admin\AdminProfileController;
Route::get('/profile', [AdminProfileController::class, 'edit'])
->middleware('auth:admin')
->name('profile.edit');
Route::post('/profile', [AdminProfileController::class, 'update'])
->middleware('auth:admin')
->name('profile.update');
app/Http/Controllers/Admin/AdminProfileController.php:
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AdminProfileController extends Controller
{
public function edit()
{
$admin = auth('admin')->user();
return view('admin.profile', compact('admin'));
}
public function update(Request $request)
{
$admin = auth('admin')->user();
$request->validate([
'name' => ['required', 'string', 'max:255'],
'password' => ['nullable', 'min:8', 'confirmed'],
]);
$admin->name = $request->name;
if ($request->filled('password')) {
$admin->password = Hash::make($request->password);
}
$admin->save();
return back()->with('success', 'Profile updated successfully!');
}
}
resources/views/admin/profile.blade.php:
<!doctype html>
<html>
<head>
<title>Admin Profile</title>
</head>
<body>
<h2>Admin Profile</h2>
@if(session('success'))
<p style="color:green;">{{ session('success') }}</p>
@endif
<form method="POST" action="{{ route('admin.profile.update') }}">
@csrf
<div>
<label>Name</label>
<input type="text" name="name" value="{{ old('name', $admin->name) }}" required>
</div>
<hr>
<p><b>Change Password</b> (optional)</p>
<div>
<label>New Password</label>
<input type="password" name="password">
</div>
<div>
<label>Confirm New Password</label>
<input type="password" name="password_confirmation">
</div>
<button type="submit">Save Changes</button>
</form>
<p><a href="{{ route('admin.dashboard') }}">Back to Dashboard</a></p>
</body>
</html>
If you used Breeze, you likely already have /dashboard and /profile.
But for a “middleware-based tutorial feel”, here’s the pattern I recommend:
Route::prefix('user')->name('user.')->middleware('auth')->group(function () {
Route::get('/dashboard', function () {
return view('user.dashboard');
})->name('dashboard');
Route::get('/profile', function () {
return view('user.profile');
})->name('profile');
});
Then your user dashboard view could use:
{{ auth()->user()->email }}
And admin views use:
{{ auth('admin')->user()->email }}
That tiny difference is everything.
Because admins aren’t registering publicly (good!), we’ll seed one.
php artisan make:seeder AdminSeeder
database/seeders/AdminSeeder.php:
<?php
namespace Database\Seeders;
use App\Models\Admin;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class AdminSeeder extends Seeder
{
public function run(): void
{
Admin::updateOrCreate(
['email' => 'admin@example.com'],
[
'name' => 'Super Admin',
'password' => Hash::make('password123'),
]
);
}
}
Run it:
php artisan db:seed --class=AdminSeeder
Now go to:
http://127.0.0.1:8000/admin/loginadmin@example.compassword123Warning ⚠️: Change that password immediately on real projects. Also, never commit real admin credentials to GitHub.
Logging in admin using Auth::attempt() (wrong guard!)
Forgetting guest:admin on admin login route (causes redirect loops)
Mixing sessions and thinking “why did admin logout log out my user too?”
Pro Tip ✅: Always prefix routes (admin/..., user/...). It prevents confusion and keeps your app organized like a clean room.
You just built a real Laravel multi-auth system with:
users, admins)web, admin)auth, auth:admin)/adminNow your project feels like a real production app — cleaner, safer, and easier to maintain.
If you try just one thing after reading this:
✅ Add the admin guard + auth:admin middleware and protect your admin routes. That single step prevents so many “oops” moments.
You can use one login with roles, but two guards make separation cleaner. It’s like having two doors with two locks instead of one door and hoping everyone behaves.
This usually happens when:
auth instead of auth:admin (or vice versa)guest:admin on the admin login pageconfig/auth.php is incompleteYes. Admin can manage users via models and controllers normally. The key is: admin authentication stays separate, but admin can still read/write User records.
What’s the biggest struggle you’ve faced while setting up authentication in Laravel — redirects, middleware confusion, admin access, or something else? Share it in the comments (and if you want, I’ll suggest the cleanest fix).
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google
Kritim Yantra
Kritim Yantra
Kritim Yantra