You know that moment when you’re midway through building a Laravel app and suddenly realize—uh oh—you’ve tangled all your authentication logic into a knotted mess? Picture Sarah, a developer who spent two late nights wrestling with sessions, tokens, and user roles until her code cried “uncle.” She thought she had a simple “users” table and a handful of routes, but before she knew it, there were admins bumping into customers, API tokens mysteriously vanishing, and more redirect loops than she could shake a stick at. That’s when she stumbled across Laravel Guards and—honestly—it was like discovering the secret sauce.
Here’s the thing: Laravel Guards aren’t just some arcane checkbox in
config/auth.php. They’re the slice of pie that keeps your
application’s doors locked the right way, whether you’ve got an admin
dashboard, a mobile API, or a multi-tenant setup. By the time you
finish reading, you’ll not only know what these guards do, but you’ll
also see how to configure them so cleanly that your future self will
thank you—no more late-night confusion, pinky promise. Oh, and if
you’ve been
itching to sharpen your PHP skills, I’ve got a list of seven LeetCode alternatives waiting at the end.
Let’s get started, shall we?
If you’ve ever run php artisan make:auth (or in newer
Laravel versions, spun up Jetstream or Breeze), you’ve seen Laravel’s
built-in authentication scaffolding do its thing: register, login,
logout, password resets—the whole nine yards. Under the hood, there’s
Auth::attempt(), the session driver, and an Eloquent user
provider tied to your users table. It feels magical—enter
your credentials, and Laravel hands you a session cookie that keeps
you logged in. That’s fine when you only have one user type, but what
happens when your app needs an admin area, a separate vendor
dashboard, or a public API with its own token mechanics?
Let’s be real: without guards, you start customizing your middleware,
fiddling with Auth::user(), and before you know it,
someone’s accidentally referencing the wrong model, or you’re sending
password resets to the wrong table. Guards are that next layer. Think
of them as gatekeepers that say, “Hey—I’m the guard for admins,” or
“I’m the guard for API requests,” each with its own rules for checking
who’s allowed in. They work in tandem with providers (which define
where your users live) and drivers (which define
how Laravel should authenticate them). In short, guards keep
your various authentication paths from stepping on each other’s toes.
At its simplest, a Laravel Guard is a class (often built-in) that
manages how users are authenticated for each request. When you call
something like Auth::guard('admin')->user(), you’re
asking Laravel, “Hey, guard named ‘admin’, show me the logged-in admin
user if there is one.” Without specifying a guard, Laravel defaults to
whatever guard you set in auth.defaults.guard (usually
web).
Guards have three main responsibilities:
users or
admins model) and returns the matching user instance.
auth:guardName kicks in, the guard decides whether the
current request is authenticated. If not, you get redirected or
receive a 401.
If it helps, imagine a guard like a Hall Monitor in a school: they check IDs (providers), verify attendance (drivers), and decide who gets into the principal’s office (request checks).
Provider = where your user records live (Eloquent
model, database table, etc.).
Driver = how the guard actually checks credentials
(session, token, Passport, etc.).
Guard = pairs a driver with a provider.
In config/auth.php, you’ll see something like:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
When a request hits a route with auth:api, Laravel’s API
guard (using token driver) looks at the header, checks
api_token in the users table (via the
users provider). Boom—user authenticated or not.
By default, Laravel sets up a web guard that uses the
session driver and the users provider. This
is your classic session-based auth: user logs in, Laravel tucks their
user ID into the session, and a cookie keeps them logged in until they
log out or the session expires. It’s what you want for traditional
server-rendered apps. The middleware group named web
(which adds session start, CSRF, etc.) works hand-in-hand with this
guard.
Heads-Up: If you ever log in under the
web guard, calling Auth::user() anywhere
(views, controllers, Blade templates) gives you that logged-in “User”
instance. But if you have another guard—say, admin—Auth::user()
might return null unless admin is your
default guard.
For API routes, Laravel ships with a simple token driver
that checks a user’s api_token column. That’s fine for
quick, small-scale stuff, but you might hit a wall if you need token
scopes, refresh tokens, or OAuth2 features. That’s where Laravel
Passport struts in: it spins up a full OAuth2 server within your app,
complete with client IDs, secret keys, and token scopes—fancy stuff.
Then there’s Laravel Sanctum, which some folks prefer for single-page apps (SPAs) or mobile backends. It’s more lightweight than Passport, lets you issue multiple tokens per user, and even includes the option to use session-based cookies for SPA auth. If you’re building a React-or-Vue front end that calls a Laravel API, Sanctum often feels more straightforward than Passport.
What if your app needs to talk to an LDAP server or an external SSO? You can roll your own guard. The basic steps:
Illuminate\Contracts\Auth\Guard.
AuthServiceProvider, call
Auth::extend('customDriverName', function($app, $name, array
$config) { … });.
config/auth.php under
'driver' => 'customDriverName'.
I once built a guard for a client who had a legacy Oracle DB of users (hey, it happens). Our custom guard fetched credentials from the Oracle table, validated them, then returned a Laravel user instance. It sounds complicated, but once you get the hang of Laravel’s guard contract, it’s surprisingly straightforward (and kinda fun, if you’re into that sort of thing).
config/auth.php the Right Wayguards Array, One Key at a Time
If you peek into config/auth.php, you’ll find something
like this by default:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
- Key Name (web, api) = the
guard’s identifier. When you write Auth::guard('api'),
Laravel looks for this configuration.
- driver (session, token,
passport, sanctum, or your custom driver) =
how to authenticate.
- provider (users, admins)
= which user source to query.
- hash (only for token driver) = tells
Laravel if stored tokens are hashed. If true, Laravel
runs Hash::check($plainToken, $hashedTokenInDb).
Often you end up with more than “just users.” Maybe you have admins,
vendors, writers—whatever. You can extend that default
guards array to something like:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
'vendor' => [
'driver' => 'session',
'provider' => 'vendors',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
Then you add providers:
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
'vendors' => [
'driver' => 'eloquent',
'model' => App\Models\Vendor::class,
],
],
Now you can do
Auth::guard('admin')->attempt($credentials) or
Auth::guard('vendor')->user(), and Laravel knows exactly
where and how to check.
Naming Tips:
- Keep keys descriptive but concise—admin,
vendor, customer—so you don’t end up
scratching your head later.
- Match your provider names to your models: if your model is
Vendor, the provider key vendors makes
sense.
- Be consistent: don’t mix singular and plural arbitrarily, or you’ll
forget which one you used.
- Use session driver for any classic web
interface: admin dashboards, user portals—anything that relies on
cookies and state.
- Use token driver for very small APIs
where you just run
php artisan make:migration add_api_token_to_users_table,
generate a random string, and call it a day.
- Use Passport when you need OAuth2 features—like
issuing access & refresh tokens, scoping, third-party logins,
etc.
- Use Sanctum if you want the best of both worlds:
session cookies for SPAs and simple token issuance for mobile apps.
Heads-Up: If you pick passport or
sanctum, be sure to
follow their respective installation guides—there are extra steps like migrations, service providers, and
publishing config files.
auth:guardName Middleware
One of the slickest things about Laravel is how easy it is to slap
middleware on route groups. Suppose you have an admin dashboard—just
wrap it in auth:admin like so:
Route::prefix('admin')
->middleware(['auth:admin'])
->group(function () {
Route::get('/dashboard', [AdminController::class, 'dashboard']);
// …more admin routes
});
Laravel says, “Alright, guard ‘admin’, check if you’ve got a logged-in user. If not, toss ’em back to the admin login.” No more copy-pasting guard checks inside each controller method; middleware does it all at once.
You can even pass multiple guards, like
['auth:admin,web'], which means “Let either the admin or
the regular user in.” But be careful—if you do that, you need to know
which guard gave the OK, or you might end up with an unexpected model
in Auth::user().
Sometimes you need fine-grained control inside your controller. Maybe you want to fetch the vendor object if they’re logged in, or redirect them somewhere else. Easy:
public function showVendorDashboard()
{
if (Auth::guard('vendor')->check()) {
$vendor = Auth::guard('vendor')->user();
return view('vendor.dashboard', compact('vendor'));
}
return redirect()->route('vendor.login');
}
Notice that if you just used Auth::user(), you might grab
the web user instead of the vendor.
Controllers—especially in multi-auth—should specify the guard each
time, unless you set the middleware to ensure the right guard is in
place.
Imagine a login form where you pick your “role” via a dropdown: admin,
vendor, or customer. You could write a custom middleware called
DynamicGuard that looks at
$request->input('role') and runs
Auth::shouldUse($role). Here’s a rough sketch:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class DynamicGuard
{
public function handle($request, Closure $next)
{
$role = $request->input('role'); // e.g., 'admin' or 'vendor'
if (in_array($role, ['admin', 'vendor', 'web'])) {
Auth::shouldUse($role);
}
return $next($request);
}
}
Then register it in Kernel.php and slap it on your login
route:
Route::post('/login', [AuthController::class, 'login'])
->middleware('dynamic.guard');
Now, when users submit role=admin, Laravel switches the
default guard to admin for that request. Neat, right? It
feels almost magical—like your app adapts on the fly.
Let’s paint a picture: you’re building a platform that sells courses.
You have instructors (they create courses), students (they enroll),
and site admins (they manage everything behind the scenes). If you
shoehorn them all into the same users table with a
role column, you’ll end up writing a mess of
if ($user->role === 'instructor') inside your
controllers and views. Instead, you can create three
tables—instructors, students,
admins—each with its own Eloquent model
(Instructor, Student, Admin)
and its own guard. That way, when an instructor logs in, it’s always
clear you’re calling Auth::guard('instructor')->user(),
and never accidentally pulling a student record.
Let’s say an instructor and a student happen to use the same browser
on a shared computer (it happens!). If you log in as an instructor
under the instructor guard, Laravel will set the session
key under something like login_instructors_1234_cookie.
If the student later logs in under the student guard,
they get a separate session cookie. Neither one stomps on the other
because guards isolate those cookie names and session keys.
Heads-Up: If you ever customize
session.cookie in config/session.php, make
sure you don’t accidentally unify cookie names across guards—otherwise
you negate the whole point of guard isolation.
Password resets can be a bit of a headache if you’ve got multiple user
types. In config/auth.php, you’ll see something like:
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
],
If you need resets for admins, extend it:
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
'admins' => [
'provider' => 'admins',
'table' => 'admin_password_resets',
'expire' => 30,
],
],
Then in your AdminForgotPasswordController, call
Password::broker('admins')->sendResetLink($request->only('email'));
and show them a view that points to
/admin/password/reset. For email verification, you can
publish the VerificationController and tweak it to
specify ->guard('admin') so that the signed-URL logic
sends the right tokens to the right users. Push these details aside at
your own peril—forgotten providers/ brokers are a classic source of
confusion.
config/auth.php
Let me tell you about the time I spent two hours scratching my head
because Laravel threw “Auth guard [manager] is not defined.” I had
written auth:manager in my route, but I never added the
manager guard in config/auth.php. Rookie
mistake. The solution was simple—add:
'guards' => [
'manager' => [
'driver' => 'session',
'provider' => 'managers',
],
// ... existing guards
],
Always double-check that the guard key in your middleware or
Auth::guard('manager') call lines up exactly with the
guard you registered.
Auth::user() When You Meant
Auth::guard('admin')->user()
In a multi-guard setup, Auth::user() can be a trap. By
default, it’s shorthand for
Auth::guard('web')->user() (or whatever your default
guard is). So if you had someone log in as an admin under
Auth::guard('admin'), calling
Auth::user() in your Blade view would return
null. You might then try to check
$user->name and slam head-first into a “trying to get
property of non-object” fatal error. Instead, either call
Auth::guard('admin')->user() explicitly or—if you’re sure
middleware has already forced the right guard—use
auth()->user() but ensure the default guard is set
appropriately before that request.
When you use the token driver for API authentication, you
have to decide whether your api_token should be stored
plain-text or hashed. If you set 'hash' => true in
config/auth.php for your api guard, Laravel
expects the api_token column to contain a hashed token
(Hash::make($token)). When you call
Auth::guard('api')->user(), Laravel runs
Hash::check($providedToken, $hashedTokenInDb). If you
don’t hash and you set 'hash' => true, it simply won’t
match. Conversely, if you store plain tokens but leave
'hash' => false, your code might inadvertently accept
partial matches or be vulnerable. My rule of thumb: if your tokens are
truly secret, hash them; otherwise, stick with
hash => false but be mindful of security implications.
In config/auth.php, you’ll see:
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
If someone on your team thought it would be clever to switch the
default to admin because “most pages are for admins,”
you’ll break every user login that relies on web.
Suddenly, Auth::attempt($credentials) logs someone into
admin guard, and your /home route is looking
for a web user and can’t find one—redirection loops
ensue, tickets fly around, and productivity drops. Always think twice
before changing that default; if you need to use
admin everywhere, consider explicitly referring to
auth:admin
instead.
Meet BrightCourses, a made-up platform that sells customizable educational portals to companies. There are three main user roles:
Without separate guards, all these roles would end up mashed into one table or one guard, and you’d quickly run into “I logged in as a student, but the system says I’m an admin” errors. Let’s see how to architect this cleanly.
When I worked on a similar project, we started with a whiteboard
session (coffee cups strewn everywhere, half-empty notebooks). We
decided on three tables: super_admins,
tenant_admins, users (for students). Each
table had id, name, email,
password, and—for tenant admins and users—a
tenant_id to link them to a company. We also added an
api_token field to users so students could
access the mobile app via tokens.
On the Eloquent side, we created three models:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class SuperAdmin extends Authenticatable
{
protected $guard = 'superadmin';
protected $fillable = ['name', 'email', 'password'];
}
class TenantAdmin extends Authenticatable
{
protected $guard = 'tenantadmin';
protected $fillable = ['name', 'email', 'password', 'tenant_id'];
}
class User extends Authenticatable
{
protected $guard = 'web';
protected $fillable = ['name', 'email', 'password', 'tenant_id', 'api_token'];
}
We knew right away these guards would help us keep sessions apart: a super admin’s session should never be confused with a tenant admin or a student.
config/auth.php for Multi-Tenant FlowOur config/auth.php ended up looking like this:
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'superadmin' => [
'driver' => 'session',
'provider' => 'superadmins',
],
'tenantadmin' => [
'driver' => 'session',
'provider' => 'tenantadmins',
],
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
'providers' => [
'superadmins' => [
'driver' => 'eloquent',
'model' => App\Models\SuperAdmin::class,
],
'tenantadmins' => [
'driver' => 'eloquent',
'model' => App\Models\TenantAdmin::class,
],
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
'tenantadmins' => [
'provider' => 'tenantadmins',
'table' => 'tenant_admin_password_resets',
'expire' => 30,
],
'superadmins' => [
'provider' => 'superadmins',
'table' => 'superadmin_password_resets',
'expire' => 30,
],
],
Notice how each guard has its matching provider. The
api guard points to Passport so that students can use
tokens in the mobile app. Because the default guard is
web, any time we call Auth::user() in a
student-facing route, it will use the
users provider—exactly what we want.
For super admins, we created a route file
routes/superadmin.php and added this group:
Route::prefix('superadmin')
->name('superadmin.')
->middleware(['auth:superadmin', 'verified'])
->group(function () {
Route::get('/dashboard', [SuperAdminController::class, 'dashboard']);
// …more superadmin routes
});
Similarly for tenant admins, in routes/tenantadmin.php:
Route::prefix('tenantadmin')
->name('tenantadmin.')
->middleware(['auth:tenantadmin', 'verified', 'bind.tenant'])
->group(function () {
Route::get('/dashboard', [TenantAdminController::class, 'dashboard']);
// …more tenant admin routes
});
We also added a bind.tenant middleware that checked if
the tenant_id in the session matched the
tenant_id in the URL—for an extra layer of sanity.
For students (end users), our web.php looked like:
Route::middleware('auth:web')
->group(function () {
Route::get('/courses', [CourseController::class, 'index']);
// …more student routes
});
If anyone tried to sneak into
/tenantadmin/dashboard without being logged in as a
tenant admin, Laravel redirected them to
/tenantadmin/login. No confusion, no fuss.
In resources/views/layouts/app.blade.php, we included
logic at the top to conditionally show navbars:
@if (Auth::guard('superadmin')->check())
@include('partials.nav-superadmin')
@elseif (Auth::guard('tenantadmin')->check())
@include('partials.nav-tenantadmin')
@elseif (Auth::guard('web')->check())
@include('partials.nav-user')
@else
@include('partials.nav-guest')
@endif
That way, super admins saw menus for user analytics, subscription metrics, and global settings. Tenant admins saw course-creation links, “invite student” buttons, and a list of instructors. Students got a streamlined menu: “My Courses,” “Profile,” “Logout.” If you tried to peek under the wrong navbar, you simply didn’t see the links in the first place—self-documenting UI, basically.
We wrote PHPUnit feature tests like so:
public function test_tenant_admin_cannot_access_superadmin_dashboard()
{
$tenantAdmin = TenantAdmin::factory()->create();
$this->actingAs($tenantAdmin, 'tenantadmin')
->get('/superadmin/dashboard')
->assertRedirect('/superadmin/login');
}
public function test_student_cannot_access_tenantadmin_routes()
{
$student = User::factory()->create();
$this->actingAs($student, 'web')
->get('/tenantadmin/dashboard')
->assertRedirect('/tenantadmin/login');
}
public function test_superadmin_can_access_superadmin_dashboard()
{
$superadmin = SuperAdmin::factory()->create();
$this->actingAs($superadmin, 'superadmin')
->get('/superadmin/dashboard')
->assertStatus(200);
}
Once these passed, we knew our guard logic was bulletproof. If a new
developer joined the team and asked, “So how do I let the vendor see a
different page?” we could point them to
Auth::guard('vendor') and they’d immediately grasp the
pattern.
Stepping back from all the code and config, here’s the bottom line: Laravel Guards give you the power to say, “This is how an admin logs in; that is how a user logs in; and this is how the API authenticates requests.” When you set up guards thoughtfully—defining clear providers, choosing the right drivers, isolating sessions, and testing your logic—you’ll sleep a whole lot better. No random redirect loops. No mysterious null pointers. Just robust, maintainable authentication that scales with your application’s needs.
So, what’s next? Take a moment this week to review your
config/auth.php. Is there a guard you’re not using? Is
there an edge case you haven’t tested? Maybe whip up a little
proof-of-concept where you add a vendor guard and see how
easy it is to secure a new set of routes. While you’re at it, pick one
of those LeetCode alternatives—say, Codewars—and solve a PHP kata or
two. Before you know it, you’ll be breezing through guard
configurations and writing elegant Eloquent queries like it’s second
nature.
You’ve got the tools, you’ve got the code snippets, and you’ve got a map of the pitfalls to avoid. Now go lock down your application—confidently, clearly, and with a bit of swagger. Because once you’ve mastered Laravel Guards, you’ll look at authentication like a seasoned pro (and—trust me—you’ll sleep better knowing your gates are firmly closed).
If you enjoyed this article, check out our latest post on 7 of the best free LeetCode alternatives. As always, if you have any questions or comments, feel free to contact us.