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 stringemail
/username
and apassword
. The name of the email / username field should match theusername
value within theconfig/fortify.php
configuration file. In addition, a booleanremember
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.