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

1# The -m is to create a migration along with it.
2php 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.

1Schema::create('admins', function (Blueprint $table) {
2 $table->increments('id');
3 $table->string('name');
4 $table->string('email')->unique();
5 $table->string('password');
6 $table->rememberToken();
7 $table->timestamps();
8});

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

1/**
2* Define the "web" routes for the application.
3*
4* These routes all receive session state, CSRF protection, etc.
5*
6* @return void
7*/
8protected function mapControlCenterRoutes()
9{
10 Route::middleware('web')
11 ->as('control-center.')
12 ->prefix('control-center')
13 ->namespace($this->namespace . '\\ControlCenter')
14 ->group(base_path('routes/control-center.php'));
15}

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

1<?php
2 
3Route::view('/', 'control-center.home')->middleware('auth:admin')->name('home');
4 
5Route::get('login', 'LoginController@showLoginForm');
6Route::post('login', 'LoginController@login')->name('login');
7Route::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

1'admin' => [
2 'driver' => 'session',
3 'provider' => 'admins',
4]

and this to the providers array

1'admins' => [
2 'driver' => 'eloquent',
3 'model' => App\Admin::class,
4]

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.

1<?php
2 
3namespace App\Http\Controllers\ControlCenter;
4 
5use Illuminate\Http\Request;
6use App\Http\Controllers\Controller;
7use Illuminate\Support\Facades\Auth;
8use Illuminate\Foundation\Auth\AuthenticatesUsers;
9 
10class LoginController extends Controller
11{
12 use AuthenticatesUsers;
13 
14 /**
15 * Where to redirect users after login.
16 *
17 * @var string
18 */
19 protected $redirectTo = '/control-center';
20 
21 /**
22 * Create a new controller instance.
23 *
24 * @return void
25 */
26 public function __construct()
27 {
28 $this->middleware('guest:admin')->except('logout');
29 }
30 
31 /**
32 * Show the application's login form.
33 *
34 * @return \Illuminate\Http\Response
35 */
36 public function showLoginForm()
37 {
38 return view('control-center.auth.login');
39 }
40 
41 /**
42 * Get the guard to be used during authentication.
43 *
44 * @return \Illuminate\Contracts\Auth\StatefulGuard
45 */
46 protected function guard()
47 {
48 return Auth::guard('admin');
49 }
50}

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

1use Illuminate\Auth\AuthenticationException;
2 
3protected function unauthenticated($request, AuthenticationException $exception)
4{
5 if ($request->expectsJson()) {
6 return response()->json(['error' => 'Unauthenticated.'], 401);
7 }
8 
9 if ($request->is('control-center') || $request->is('control-center/*')) {
10 return redirect()->guest('/control-center/login');
11 }
12 
13 return redirect()->guest(route('login'));
14}
  • #### 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

1if (Auth::guard($guard)->check()) {
2 return redirect('admin' == $guard ? '/control-center' : '/home');
3}
  • #### 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

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

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

1use App\Http\Middleware\ChangeAuthDriverForControlCenter;
2 
3Route::middleware(['web', ChangeAuthDriverForControlCenter::class)
4 ->as('control-center.')
5 ->prefix('control-center')
6 ->namespace($this->namespace . '\\ControlCenter')
7 ->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 ✌️