Implementing multi-guard authentication in Laravel

I wanna touch on something that I've been wanting to for a long time, and that is multi-guard authentication in Laravel.

In the past when I'd write an app that has both normal users and admin users, I would keep them together in the same table, and use either an extra type column to differentiate them, or use a package like Spatie's Laravel Permission, but both felt a little ugly to deal with. But in fact, Laravel since 5.2 (maybe 5.3) shipped with the ability to have multi-guard authentication. let's see how we can implement this.

Preparing Model and Table

We want to separate the users from admins in almost every way, so let's start with a model and a table, we can whip these up using Artisan

# The -m is to create a migration along with it.
php artisan create:model Admin -m

and then we'll fill the migration (_create_admins_table.php) with a couple of columns, modify this to your app's requirements, and then run php artisan migrate to have our database ready.

Schema::create('admins', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});

Preparing Routes

Of course, we'll need routes for the "control-center", we'll create a separate routes file and load it with its settings. To load a new routes file, we need to update App\Providers\RouteServiceProvider.php and add the following method

/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapControlCenterRoutes()
{
Route::middleware('web')
->as('control-center.')
->prefix('control-center')
->namespace($this->namespace . '\\ControlCenter')
->group(base_path('routes/control-center.php'));
}

what this basically do is - apply the web middleware, we need this to enable sessions, etc ... - as() just namespaces the route names, so that we can reference routes like this route('control-center.login') - prefix() the routes inside, ex. /control-center/login - namespace() is to namespace the controller lookup, so all of these routes controllers will be expected to be in App\Http\Controllers\ControlCenter - and finally, point to the routes file we want

and then we need to call this method in the map() method in the same class.

next, we'll create the control-center.php file, let's just include the admin login routes

<?php
 
Route::view('/', 'control-center.home')->middleware('auth:admin')->name('home');
 
Route::get('login', 'LoginController@showLoginForm');
Route::post('login', 'LoginController@login')->name('login');
Route::post('logout', 'LoginController@logout')->name('logout');

as you might have noticed (did you?) from above we're using an auth:admin middleware, this tells laravel to pass the "admin" parameter as the guard name to the auth middleware, but currently we don't have a guard named admin, so let's create it.

Preparing Guards

As I've mentioned in the beginning, Laravel supports multi-guard authentication out of the box, we'll just need to edit a couple of lines, let's hope in config\auth.php

add this to the guards array

'admin' => [
'driver' => 'session',
'provider' => 'admins',
]

and this to the providers array

'admins' => [
'driver' => 'eloquent',
'model' => App\Admin::class,
]

Preparing the Login Controller

Now Laravel already give you complete authentication scaffolding for free, and rolling your own guard doesn't mean that you re-write the logic again, it gives you this trait Illuminate\Foundation\Auth\AuthenticatesUsers on a plate of gold, it basically has all the logic you need, you just extend what you need.

<?php
 
namespace App\Http\Controllers\ControlCenter;
 
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
 
class LoginController extends Controller
{
use AuthenticatesUsers;
 
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/control-center';
 
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:admin')->except('logout');
}
 
/**
* Show the application's login form.
*
* @return \Illuminate\Http\Response
*/
public function showLoginForm()
{
return view('control-center.auth.login');
}
 
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\StatefulGuard
*/
protected function guard()
{
return Auth::guard('admin');
}
}

as you can see we only customized three tiny methods and have a full-featured login controller, with throttling and all the goodies, I suggest you take a look into the trait and see what you can also override, it's an architectural beauty!

Note: Similarly, Laravel provides these traits to handle other aspects of the auth system - ConfirmsPasswords - SendsPasswordResetEmails - RegistersUsers - ResetsPasswords - VerifiesEmails

Tiny Gotchas

  • #### Redirecting to /login instead of /control-center/login

If you try to access /control-center/posts while not authenticated, you'll get redirected to /login instead of /control-center/login, to solve this we need to tell the exception handler where to redirect, just override the unauthenticated() method on the App\Exceptions\Handler class

use Illuminate\Auth\AuthenticationException;
 
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
 
if ($request->is('control-center') || $request->is('control-center/*')) {
return redirect()->guest('/control-center/login');
}
 
return redirect()->guest(route('login'));
}
  • #### A similar case will happen if you try to access the /control-center/login route while authenticated, you'll be redirected to the default /home route

The logic responsible of this is in the App\Http\Middleware\RedirectIfAuthenticated.php class, depending on your case, implement the appropriate. A simple implementation would be like this

if (Auth::guard($guard)->check()) {
return redirect('admin' == $guard ? '/control-center' : '/home');
}
  • #### I have one more small thing that bugs me; accessing the guard within controllers residing in the scope of the admin guard is ugly, auth()->guard('admin')->user().

One way to fix this is to have a middleware set the default auth driver to admin, and attach this middleware to the routes file

Illuminate\Support\Facades\Auth::setDefaultDriver('admin');

and add this newly created middleware to the middleware stack in the RouteServiceProvider

use App\Http\Middleware\ChangeAuthDriverForControlCenter;
 
Route::middleware(['web', ChangeAuthDriverForControlCenter::class)
->as('control-center.')
->prefix('control-center')
->namespace($this->namespace . '\\ControlCenter')
->group(base_path('routes/control-center.php'));

Conclusion

You probably won't read this, and by now you've copy-pasted what you need.
I'd appreciate a comment down below if this post helped you, and if you have a suggestion to improve this tutorial, I'll be more than happy to have a discussion, drop me a comment.

Create something awesome, Stash out ✌️