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 TimeIf 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
MiddlewareOne 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.