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 ✌️