How I Handled Multiple Auth Guards and Redirects in Laravel
When working on a Laravel app with distinct user types, I wanted to:
- Let both
User
andOffice
Model access the same dashboard routes - Add a separate
admin
guard for backend login only - Handle login redirects
Sounds simple? It wasn’t. Here’s what I learned along the way.
Phase 1 — Two Models, One Route
Goal: Allow both User
and Office
to access /dashboard/courses
.
❌ My first approach: Create a custom middleware:
public function handle() {
if (Auth::guard('web')->check() || Auth::guard('office')->check()) {
return $next($request);
}
return redirect()->route('login');
}
Then register it in app/Http/Kernel.php
:
protected $routeMiddleware = [
// ...
'auth.dashboard' => \App\Http\Middleware\AuthDashboardUser::class,
];
And apply it to the route:
Route::get('/dashboard/courses', [DashboardController::class, 'courses'])
->middleware('auth.dashboard');
This worked: only User
and Office
models could access the route. However, if I visited /dashboard/courses
directly while unauthenticated, I was redirected to the login page – and after logging in, I landed on /dashboard
instead of /dashboard/courses
.
Normally, when Laravel redirects you via the Authenticate
middleware, it stores the original URL in the session as url.intended
. Upon successful login, Laravel uses this value to redirect you to your original destination.
In my custom middleware, that didn’t happen — because url.intended
is only set when Laravel throws an AuthenticationException
(you could also set it manually in the session, but there might be more edge cases which break functionality.
Learning 1 — Avoid Custom Middleware
Laravel only sets url.intended
if it throws an AuthenticationException
. One could fix it by adding this to the middleware:
public function handle() {
if (Auth::guard('web')->check() || Auth::guard('office')->check()) {
return $next($request);
}
throw new \Illuminate\Auth\AuthenticationException(
'Unauthenticated.',
['web'],
route('login')
);
}
But a better way is to ditch the custom middleware entirely and use:
Route::middleware('auth:web,office')->group(function () {
Route::get('/dashboard/courses', [DashboardController::class, 'courses']);
});
The notation auth:web,office
means, it will apply the auth
middlware defined in kernel.php:
protected $routeMiddleware = [
// ...
'auth' => \App\Http\Middleware\Authenticate::class,
];
And the guards web and office are passed as parameter:
In case that the user is not authenticated with any of those guards, an AuthenticationException will be thrown which will then set the url.intended
in the session. Usign the Authenticate
middlewar will also set the current guard to the auth service, which may have also other important implications.
Scenario 2 — Separate Admin Login
Next, I needed a dedicated admin guard with a separate login route (/admin/login
) and its own section.
In config/auth.php
:
'guards' => [
'web' => [...],
'office' => [...],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
],
Then:
Route::prefix('admin')->middleware('auth:admin')->group(function () {
// admin-only routes
});
But: Laravel redirected unauthenticated admins to /login
, not /admin/login
Learning 2 — Custom Authenticate Middleware is Key
To fix the redirect, I extended Laravel’s Authenticate
middleware:
class Authenticate extends \Illuminate\Auth\Middleware\Authenticate
{
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
if ($request->is('admin*')) {
return route('voyager.login'); // or '/admin/login'
}
return route('login');
}
}
}
Then I registered it in Kernel.php
(Per default Authenticate class from vendor is taken):
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
];
Now Laravel redirects correctly depending on the route prefix.
Excerpt from Laravel’s LoginController:
protected function authenticated(Request $request, $user)
{
return redirect()->intended($this->redirectPath());
}
This is what makes url.intended
actually work post-login.