Cómo implementar autenticación para dos tipos de usuarios en Laravel: administradores y clientes

En proyectos web donde existen diferentes tipos de usuarios, una sola autenticación puede quedarse corta. Un caso común es cuando el sistema tiene un panel administrativo para colaboradores y, al mismo tiempo, una página publica donde los clientes pueden iniciar sesión para consultar o gestionar su información.

En este artículo veremos cómo estructurar en Laravel una autenticación separada para dos tipos de usuarios: administradores y clientes. La idea es mantener una arquitectura limpia, segura y fácil de mantener.


Contexto del proyecto

El escenario es el siguiente:

  • El administrador accede desde una URL privada, por ejemplo: /admin.
  • Los clientes acceden desde el sitio público, por ejemplo: /.
  • Los administradores se almacenan en la tabla users.
  • Los clientes se almacenan en la tabla customers.
  • Cada sección usa vistas, layouts, assets y controladores separados.
  • Se utiliza Laravel Breeze como punto de partida, pero se adapta la estructura para soportar dos autenticaciones independientes.

La decisión principal fue no mezclar administradores y clientes en una misma tabla, ya que tienen responsabilidades, permisos y flujos completamente diferentes.

Estructura recomendada del proyecto

Una estructura limpia puede organizarse así:

app/
├── Http/
│   ├── Controllers/
│   │   ├── Admin/
│   │   │   ├── Auth/
│   │   │   └── DashboardController.php
│   │   │
│   │   ├── Customer/
│   │   │   ├── Auth/
│   │   │   └── AccountController.php
│   │   │
│   │   └── Controller.php
│   │
│   ├── Requests/
│   │   ├── Admin/
│   │   │   └── Auth/
│   │   │       └── LoginRequest.php
│   │   │
│   │   └── Customer/
│   │       └── Auth/
│   │           └── LoginRequest.php
│
resources/
├── views/
│   ├── admin/
│   │   ├── auth/
│   │   ├── layouts/
│   │   └── dashboard/
│   │
│   └── customer/
│       ├── auth/
│       ├── layouts/
│       └── account/
│
routes/
├── admin.php
├── customer.php
├── web.php
└── console.php

public/
└── assets/
    ├── admin/
    └── customer/

Esta separación permite que cada sección tenga su propia lógica, sus propios recursos visuales y sus propios middlewares.

Configuración de guards y providers

Laravel permite manejar múltiples sistemas de autenticación mediante guards y providers.

En este caso usamos:

  • web para administradores.
  • customer para clientes.

La configuración se realiza en config/auth.php:

<?php

use App\Models\User;
use App\Models\Customer;

return [

    'defaults' => [
        'guard' => env('AUTH_GUARD', 'web'),
        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
    ],

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

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

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

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

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],

        'customers' => [
            'provider' => 'customers',
            'table' => env('CUSTOMER_AUTH_PASSWORD_RESET_TOKEN_TABLE', 'customer_password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),

];

Un detalle importante es no usar la misma variable AUTH_MODEL para ambos providers, porque podría provocar que los clientes intenten autenticarse con el modelo de usuarios administrativos.

Modelo Customer autenticable

El modelo de cliente no debe extender directamente de Model, sino de Authenticatable, igual que el modelo User.

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class Customer extends Authenticatable
{
    use Notifiable;

    protected $guard = 'customer';

    protected $fillable = [
        'name',
        'last_name',
        'phone',
        'email',
        'password',
        'enabled',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];
}

Esto permite que Laravel pueda autenticar clientes usando el guard customer.

Migraciones principales

Para los clientes se puede crear una tabla independiente:

Schema::create('customers', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('last_name');
    $table->char('phone', 10)->nullable();
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->tinyInteger('enabled')->default(1);
    $table->timestamps();
});

También se puede manejar una tabla independiente para recuperación de contraseñas:

Schema::create('customer_password_reset_tokens', function (Blueprint $table) {
    $table->string('email')->primary();
    $table->string('token');
    $table->timestamp('created_at')->nullable();
});

En cuanto a sesiones, no es obligatorio crear una tabla diferente para clientes. Laravel usa la tabla sessions para guardar la sesión del navegador, no una sesión independiente por guard.

Rutas separadas

Una buena práctica es separar las rutas del administrador y del cliente.

Ejemplo de routes/admin.php:

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Admin\DashboardController;

Route::middleware('admin.guest')->group(function () {
    Route::get('/login', [AuthenticatedSessionController::class, 'create'])
        ->name('login');

    Route::post('/login', [AuthenticatedSessionController::class, 'store'])
        ->name('login.store');
});

Route::middleware('admin.auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])
        ->name('dashboard');

    Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
        ->name('logout');
});

Ejemplo de routes/customer.php:

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Customer\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Customer\HomeController;
use App\Http\Controllers\Customer\AccountController;

Route::get('/', [HomeController::class, 'index'])
    ->name('home');

Route::middleware('customer.guest')->group(function () {
    Route::get('/login', [AuthenticatedSessionController::class, 'create'])
        ->name('login');

    Route::post('/login', [AuthenticatedSessionController::class, 'store'])
        ->name('login.store');
});

Route::middleware('customer.auth')->group(function () {
    Route::get('/mi-cuenta', [AccountController::class, 'index'])
        ->name('account');

    Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
        ->name('logout');
});

Cargar rutas desde bootstrap/app.php

En Laravel moderno, las rutas adicionales pueden registrarse desde bootstrap/app.php:

use Illuminate\Support\Facades\Route;

->withRouting(
    web: __DIR__.'/../routes/web.php',
    commands: __DIR__.'/../routes/console.php',
    then: function () {
        Route::middleware('web')
            ->prefix('admin')
            ->name('admin.')
            ->group(base_path('routes/admin.php'));

        Route::middleware('web')
            ->name('customer.')
            ->group(base_path('routes/customer.php'));
    },
)

Con esta configuración se obtienen rutas como:

  • /admin/loginadmin.login
  • /admin/dashboardadmin.dashboard
  • /logincustomer.login
  • /mi-cuentacustomer.account

Middlewares propios por tipo de usuario

Para evitar lógica condicional dentro de bootstrap/app.php, se pueden crear middlewares específicos para cada sección.

Por ejemplo:

  • admin.auth: valida que el usuario admin esté autenticado y activo.
  • admin.guest: evita que un admin autenticado vuelva al login.
  • customer.auth: valida que el cliente esté autenticado y activo.
  • customer.guest: evita que un cliente autenticado vuelva al login.

Middleware de administrador autenticado:

<?php

namespace App\Http\Middleware\Admin;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class EnsureAdminIsAuthenticated
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! Auth::guard('web')->check()) {
            return redirect()->route('admin.login');
        }

        $user = Auth::guard('web')->user();

        if (! $user->enabled) {
            Auth::guard('web')->logout();

            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect()
                ->route('admin.login')
                ->withErrors([
                    'email' => 'Tu cuenta se encuentra deshabilitada.',
                ]);
        }

        return $next($request);
    }
}

Middleware de cliente autenticado:

<?php

namespace App\Http\Middleware\Customer;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class EnsureCustomerIsAuthenticated
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! Auth::guard('customer')->check()) {
            return redirect()->route('customer.login');
        }

        $customer = Auth::guard('customer')->user();

        if (! $customer->enabled) {
            Auth::guard('customer')->logout();

            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect()
                ->route('customer.login')
                ->withErrors([
                    'email' => 'Tu cuenta se encuentra deshabilitada.',
                ]);
        }

        return $next($request);
    }
}

Después se registran los alias de middleware en bootstrap/app.php:

use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\Admin\EnsureAdminIsAuthenticated;
use App\Http\Middleware\Admin\RedirectIfAdminAuthenticated;
use App\Http\Middleware\Customer\EnsureCustomerIsAuthenticated;
use App\Http\Middleware\Customer\RedirectIfCustomerAuthenticated;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'admin.auth' => EnsureAdminIsAuthenticated::class,
        'admin.guest' => RedirectIfAdminAuthenticated::class,

        'customer.auth' => EnsureCustomerIsAuthenticated::class,
        'customer.guest' => RedirectIfCustomerAuthenticated::class,
    ]);
})

LoginRequest para clientes

Uno de los errores más comunes al trabajar con múltiples guards es usar Auth::attempt() sin especificar guard. Cuando se hace eso, Laravel utiliza el guard por defecto, normalmente web.

Para el login de clientes debe usarse:

Auth::guard('customer')->attempt($credentials, $this->boolean('remember'))

Ejemplo:

public function authenticate(): void
{
    $this->ensureIsNotRateLimited();

    $credentials = $this->only('email', 'password');
    $credentials['enabled'] = 1;

    if (! Auth::guard('customer')->attempt($credentials, $this->boolean('remember'))) {
        RateLimiter::hit($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => trans('auth.failed'),
        ]);
    }

    Auth::guard('web')->logout();

    RateLimiter::clear($this->throttleKey());
}

En este ejemplo, después de iniciar sesión como cliente, se cierra cualquier sesión activa del administrador en el mismo navegador.

LoginRequest para administradores

Para el administrador se hace lo mismo, pero usando el guard web:

public function authenticate(): void
{
    $this->ensureIsNotRateLimited();

    $credentials = $this->only('email', 'password');
    $credentials['enabled'] = 1;

    if (! Auth::guard('web')->attempt($credentials, $this->boolean('remember'))) {
        RateLimiter::hit($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => trans('auth.failed'),
        ]);
    }

    Auth::guard('customer')->logout();

    RateLimiter::clear($this->throttleKey());
}

De esta manera se evita que el mismo navegador mantenga una sesión de administrador y una sesión de cliente al mismo tiempo.

Uso correcto en Blade

Otro detalle importante es que la directiva @auth usa el guard por defecto si no se especifica uno. Por eso, en vistas de cliente se debe indicar explícitamente el guard.

Para clientes:

@auth('customer')
    Hola, {{ auth('customer')->user()->name }}
@endauth

@guest('customer')
    <a href="{{ route('customer.login') }}">Iniciar sesión</a>
@endguest

Para administradores:

@auth('web')
    Hola, {{ auth('web')->user()->name }}
@endauth

@guest('web')
    <a href="{{ route('admin.login') }}">Iniciar sesión</a>
@endguest

Sesiones en base de datos

Cuando se usa SESSION_DRIVER=database, Laravel guarda la sesión del navegador en la tabla sessions. Es importante entender que no se crea una sesión independiente por guard.

Si se inicia sesión como administrador y después como cliente en el mismo navegador, Laravel puede seguir usando el mismo registro de sesión. Lo importante es que el guard correcto esté autenticado y el otro haya sido cerrado.

Para validar:

auth('web')->check();   // Admin
auth('customer')->check(); // Cliente

También es normal que el registro en la tabla sessions permanezca por un tiempo aunque el usuario haya cerrado sesión. Laravel limpia las sesiones expiradas según su configuración de expiración.

Extras

Assets separados con Vite

Si cada sección utiliza un template diferente, también conviene separar los assets.

resources/
├── admin/
│   ├── css/
│   │   └── app.css
│   └── js/
│       └── app.js
│
└── customer/
    ├── css/
    │   └── app.css
    └── js/
        └── app.js

Ejemplo de vite.config.js:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/admin/css/app.css',
                'resources/admin/js/app.js',

                'resources/customer/css/app.css',
                'resources/customer/js/app.js',
            ],
            refresh: true,
        }),
    ],
});

En el layout de administrador:

@vite([
    'resources/admin/css/app.css',
    'resources/admin/js/app.js'
])

En el layout de cliente:

@vite([
    'resources/customer/css/app.css',
    'resources/customer/js/app.js'
])

Conclusión

Implementar autenticación para dos tipos de usuarios en Laravel requiere separar responsabilidades desde el inicio. La clave está en no mezclar modelos, guards, rutas ni vistas.

Una arquitectura limpia para este caso sería:

  • users para administradores.
  • customers para clientes.
  • web como guard de administradores.
  • customer como guard de clientes.
  • resources/views/admin para el panel administrativo.
  • resources/views/customer para el portal público.
  • routes/admin.php y routes/customer.php para separar rutas.
  • Middlewares propios para controlar accesos y redirecciones.

Esta estructura evita confusiones, mejora la seguridad y permite que el proyecto crezca de forma ordenada.

En sistemas donde existen usuarios internos y clientes finales, separar la autenticación no solo es una buena práctica técnica: también facilita el mantenimiento, las pruebas y la evolución del proyecto.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *