Multi-Guard authentication with Laravel Fortify

As I've seen, that a lot of people ask "how can I add multi auth to Laravel Fortify?" but I could not find any solution online, I thought it would be a good Idea to share my approach with you all.

First of all, I'm using a new Laravel 8 installation with the Laravel Livewire preset, created by the installer.

I'll take an administration backend as an example (as I had to create this myself with Fortify).

I like to keep things like an admin backend on a subdomain instead of a "/admin" or something alike. So first I created new config variables:

In config/app.php:

/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| your application so that it is used when running Artisan tasks.
|
*/

'base_url' => env('APP_BASEURL', 'localhost'), // NEW

'url' => env('APP_URL', 'http://localhost'),

'asset_url' => env('ASSET_URL', null),

'admin_subdomain' => env('APP_ADMIN_SUBDOMAIN', 'admin'), // NEW

Here I added "baseurl" and "adminsubdomain". These two are used to construct the domain, in order to configure Fortify.

Than I've added a helpers.php to the application containing:

(if you want a know how: Laravel News)

<?php

if (! function_exists('baseUrl')) {
    function baseUrl() {
        return config('app.base_url');
    }
}


if (! function_exists('adminUrl')) {
    function adminUrl() {
        return config('app.admin_subdomain') . '.' . baseUrl();
    }
}

These two helpers encapsulate the "logic" to construct the domains.

Now we add a macro to the request, so that we can easily check if the request was made to the admin area.

In AppServiceProvider:

<?php

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        Request::macro('isAdmin', function () {
            return $this->getHost() === adminUrl();
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

The macro has been created within the "register" method, because we need it in the FortifyServiceProvider in the "register" method, as "boot" would be too late.

So now we need a new guard. I personally used Caleb Porzio's "Parental" package, so I have the default "User" model and a derived "Admin" model, but you can do it in another way if you want.

Within config/auth.php add a new guard and provider:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

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

    'api' => [
        'driver' => 'token',
        'provider' => 'users',
        'hash' => false,
    ],
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],

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

So now we are good to go!

Let's move to the FortifySericeProvider. The only thing we need to configure to make Fortify work:

fortify.domain

fortify.guard

With fortify.domain we can instruct Fortify on which domain it registers its routes and using "fortify.guard" we tell Fortify which guard it should use for the authentication:

<?php

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        if (request()->isAdmin()) {
            config(['fortify.domain' => adminUrl()]);
            config(['fortify.guard' => 'admin']);
        }
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        // Fortify actions registration
        ....

        Fortify::loginView(function () {
            if (request()->isAdmin()) {
                return view('admin.auth.login');
            }
    
            return view('frontend.auth.login');
        });
    }
}

Within the boot method, we can change the login view used by Fortify. Note: I have separated my frontend and admin views into different folders.

In order for Laravel to know, that the requests are reaching for the admin interface, we need to use subdomain routing (see here):

In order to do so, we have two options:

We can group our routes within the routes/web.php like so:

Route::domain(adminUrl())->group(function () {
    // Your admin routes here
});

// All your other routes

Or we can (what I personally prefer) use another routes file to encapsulate all the admin routes. For this we need to create a new file: routes/admin.php

This file is no different to your routes/web.php file.

For Laravel to pick up the routes of this file, we have to modify the RouteServiceProvider:

Route::middleware('web')
    ->domain(adminUrl())
    ->group(base_path('routes/admin.php'));

Route::middleware('web')
    ->namespace($this->namespace)
    ->group(base_path('routes/web.php'));

What we have done here, is adding a new route group to the ServiceProvider. This group will automatically use the specified domain.

As stated in the docs for subdomain routing, we should make sure that the subdomain routes are registered BEFORE your other routes. From the docs:

This will prevent root domain routes from overwriting subdomain routes which have the same URI path.

And that's it!

Now we have a working admin login on a subdomain using plain Fortify!