Autenticación Multi-Guard con Laravel Fortify

Autenticación Multi-Guard con Laravel Fortify


  • Share on Pinterest

Un poco sobre Fortify

Recuerdo haber desarrollado un proyecto hace varios años con este tipo de autenticación (multi-guard) utilizando el sistema de autenticación por default en Laravel y en esta ocasión quise probar Fortify la nueva (no tan nueva ya) librería de autenticación la cual está como un paquete opcional a la instalación de Laravel y que trae cosas interesantes como:

  • Login, registro, verificación de cuenta vía email, autenticación de 2 factores y recuperación de contraseñas.
  • Es headless, esto es que, no incluye vistas haces las tuyas y solo le envías a la ruta correcta por ejemplo /login los campos del form de logín y listo:

    “Your login template should include a form that makes a POST request to /login. The /login endpoint expects a string email / username and a password. The name of the email / username field should match the username value within the config/fortify.php configuration file. In addition, a boolean remember field may be provided to indicate that the user would like to use the “remember me” functionality provided by Laravel.”

Contexto del ejemplo

En el proyecto en el que estoy trabajando actualmente cuenta con dos tipos de usuarios: Administradores y Clientes. Dada la complejidad de cada uno decidí separarlos en dos modelos: User (para los Administradores) y Customer (para los Clientes) los modelos obviamente en tablas independientes. Ambos modelos debían tener capacidades similares como login, registro, recuperación de contraseña y en el caso de Customer activación de la cuenta vía e-mail.

Configuración para multi-guard con Fortify

Instalación de Fortify

Para instalar Fortify es necesario Laravel 8 en adelante.

https://laravel.com/docs/8.x/fortify#installation
(Perdón pero en el artículo es sobre configurar, no instalar 😉 )

Antes de empezar

Esta es una forma de implementar la funcionalidad de multi-guard que encontré en internet y haciendo pruebas es la que a mi me funcionó, no sé si exista una mejor manera si la conoces te invito a que me lo hagas saber por medio de twitter @guillermo_px.

Configuración de los modelos

Como mencioné anteriormente para mi caso creé dos modelos uno llamado User que es el que trae Laravel por default y Customer creado a partir de Users (copy-paste para rápido).

Models/User.php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

    protected $fillable = [
        'name',
        'email',
        'password',
        'active',
    ];

Models/Customer.php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Contracts\Auth\MustVerifyEmail;

class Customer extends Authenticatable implements MustVerifyEmail
{

    ...

    protected $fillable = [
        'uuid',
        'first_name',
        'last_name',
        'email',
        'password',
        'phone',
        'active',
        'plan_id',
    ];

Es importante notar que, para que un modelo pueda ser usado para autenticar sus registros debe de extender de “Authenticatable” y para el caso de requerir verificación por e-mail debe de implementar la interfaz “MustVerifyEmail” como en el modelo Customer.

Configuración del archivo auth.php

Toca el turno de configurar el archivo auth.php donde se añadirá el guard que se requiere:

<?php

return [

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

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

        'customer' => [
            'driver' => 'session',
            'provider' => 'customers',
        ],
    ],

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

        'customers' => [
            'driver' => 'eloquent',
            'model' => App\Models\Customer::class,
        ],

    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        'customers' => [
            'provider' => 'customers',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => 10800,

];

Así es como se ve mi archivo config/auth.php después de configurar el guard para el modelo Customer, líneas 16, 28 y 42.

Configuración de Fortify

Es momento de configurar Fortify para la autenticación muli-guard, nuestro punto de partida será el archivo config/fortify.php aunque no se modificará este archivo servirá como referencia. Así se ve por default (sin los comentarios):

<?php

use App\Providers\RouteServiceProvider;
use Laravel\Fortify\Features;

return [

    'guard' => 'web',

    'passwords' => 'users',

    'username' => 'email',

    'email' => 'email',

    'home' => RouteServiceProvider::HOME,

    'prefix' => '',

    'domain' => null,

    'middleware' => ['web'],

    'limiters' => [
        'login' => 'login',
        'two-factor' => 'two-factor',
    ],

    'views' => true,

    'features' => [
        Features::registration(),
        Features::resetPasswords(),
        Features::emailVerification(),
        // Features::updateProfileInformation(),
        Features::updatePasswords(),
        // Features::twoFactorAuthentication([
        //     'confirmPassword' => true,
        // ]),
    ],

];

Ahora, toda la configuración se concentrará en el archivo app/Providers/FortifyServiceProvider.php

Así se ve el archivo por default:

<?php

namespace App\Providers;

use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{

    public function register()
    {
        //
    }

    public function boot()
    {
        Fortify::createUsersUsing(CreateNewUser::class);
        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

        RateLimiter::for('login', function (Request $request) {
            $email = (string) $request->email;

            return Limit::perMinute(5)->by($email.$request->ip());
        });

        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });
    }
}

Así se ve después de agregar toda la configuración:

<?php

namespace App\Providers;

use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\CreateNewCustomer;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Features;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        if (request()->is('admin', 'admin/*')) {
            $this->adminConfig();
        } else {
            $this->customerConfig();
        }
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

        RateLimiter::for('login', function (Request $request) {
            $email = (string) $request->email;

            return Limit::perMinute(5)->by($email . $request->ip());
        });

        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });

        if (request()->is('admin', 'admin/*')) {
            $this->adminFotifyConfig();
        } else {
            $this->customerFotifyConfig();
        }
    }

    // Register --------
    private function adminConfig()
    {
        config(['fortify.features' => [
            Features::resetPasswords(),
            Features::updatePasswords(),
        ]]);

        config(['fortify.prefix' => 'admin']);
        config(['fortify.home' => '/admin/dashboard']);
        config(['fortify.redirects.login' => '/admin/dashboard']);
        config(['fortify.redirects.logout' => '/admin']);
    }

    private function customerConfig()
    {
        config(['fortify.features' => [
            Features::registration(),
            Features::resetPasswords(),
            Features::emailVerification(),
            Features::updatePasswords(),
        ]]);

        config(['fortify.guard' => 'customer']);
        config(['fortify.prefix' => 'user']);
        config(['fortify.home' => '/user/dashboard']);
        config(['fortify.passwords' => 'customers']);
        config(['fortify.redirects.login' => '/user/dashboard']);
        config(['fortify.redirects.logout' => '/user/login']);
    }

    // Boot --------
    private function adminFotifyConfig()
    {
        Fortify::createUsersUsing(CreateNewUser::class);
        Fortify::loginView('admin.auth.login');
    }

    private function customerFotifyConfig()
    {
        Fortify::createUsersUsing(CreateNewCustomer::class);
        Fortify::loginView('customer.auth.login');
        Fortify::registerView('customer.auth.register');
        Fortify::verifyEmailView('customer.auth.verify-email');
        Fortify::requestPasswordResetLinkView('customer.auth.forgot-password');
        Fortify::resetPasswordView('customer.auth.reset-password');
    }
}

Explicación:

Toda mi configuración la dividí en 4 métodos y estos se ejecutan en “register” y “boot” del service provider.

Los métodos que se ejecutan en register:

private function adminConfig() { ... }
private function customerConfig() { ... }

Estos métodos se encargan de actualizar el archivo de configuración de Fortify (config/fortify.php) con la configuración necesaria y que es más crítica para Fortify.

Los métodos que se ejecutan en boot:

private function adminFotifyConfig() { ... }
private function customerFotifyConfig() { ... }

Estos métodos son responsables de definir las actions y las vistas para cada operación como por ejemplo login o registro.

Las actions son simplemente clases que tienen una única funcionalidad y con un único método por ejemplo este es el action para el registro de Customer:

<?php

namespace App\Actions\Fortify;

use App\Models\Customer;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewCustomer implements CreatesNewUsers
{
    use PasswordValidationRules;

    public function create(array $input)
    {
        Validator::make($input, [
            'first_name' => ['required', 'string', 'max:255'],
            'last_name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:customers'],
            'password' => $this->passwordRules(),
        ])->validate();

        return Customer::create([
            'first_name' => $input['first_name'],
            'last_name' => $input['last_name'],
            'email' => $input['email'],
            'password' => Hash::make($input['password']),
        ]);
    }
}

Las actions se ubican en el directorio app/actions y en el caso de las actions para Fortify en app/actions/Fortify . Es muy probable que no exista el directorio de actions después de la instalación de Fortify, en ese caso, el directorio lo puedes crear tu mismo y copiar las actions que necesites modificar desde de la instalación de Fortify en vendor: vendor/laravel/fortify/src/Actions. También puedes crear tus propias actions según lo que requieras.

Paréntesis

Las actions están tomando relevancia en el mundo de Laravel ya que permiten tener un código mucho más desacoplado y reutilizable, si te interesa el tema te recomiendo la platica de Luke Downing en la última Laracon:

https://youtu.be/0Rq-yHAwYjQ?t=1728

Retomando la explicación

La magia detrás de esta configuración es simplemente actualizar los valores del archivo de configuración de Fortify (config/fortify.php):

Tomaré de ejemplo la configuración del guard de Customer:

    private function customerConfig()
    {
        config(['fortify.features' => [
            Features::registration(),
            Features::resetPasswords(),
            Features::emailVerification(),
            Features::updatePasswords(),
        ]]);

        config(['fortify.guard' => 'customer']);
        config(['fortify.prefix' => 'user']);
        config(['fortify.home' => '/user/dashboard']);
        config(['fortify.passwords' => 'customers']);
        config(['fortify.redirects.login' => '/user/dashboard']);
        config(['fortify.redirects.logout' => '/user/login']);
    }

Primero, de la línea 3 a 8, activamos las funcionalidades de Fortify que necesito para ese guard en específico, posteriormente de la línea 10 a 15 actualizo los valores del archivo config/fortify.php:

  • fortify.guard: Este es el guard que registré en el archivo de configuración config/auth.php
  • fortify.prefix: El prefijo que tendrán las rutas, por ejemplo: /user/login o /user/register
  • fortify.passwords: Como se gestionarán los password para este guard, también configurado en config/auth.php
  • fortify.redirects.login: A donde se redireccionará al usuario después de hacer login
  • fortify.redirects.logout: A donde se redireccionará al usuario después de cerrar sesión

Para determinar que configuración utilizar para un guard, utilizó una condición que me permita saber que métodos ejecutar dependiendo de la ruta:

...
if (request()->is('admin', 'admin/*')) {
    $this->adminConfig();
} else {
    $this->customerConfig();
}
...

Rutas

No es necesario registrar ninguna ruta adicional todo lo hace Fortify dependiendo que funcionalidades estén habilitadas en “features” en la configuración de Fortify.

Conclusión

Fortify fue una grata sorpresa, me gustó la facilidad con la que puedes implementar todo el sistema de autenticación fuera de la caja y además que es muy fácil intercambiar sus funcionalidades por las tuyas por medio de actions, además se agradece el hecho de que existe un archivo de configuración.