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

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'));
}

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');
}

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