Implementing multi-guard authentication in Laravel
Yazan Stash • March 27, 2020
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 ✌️