Saltearse al contenido

07 - Mapa de Autenticación y Autorización

Validated Corrections

  • GET /checkout si esta protegido por cliente_registro_completo y verificar_cliente_presenta_mora, pero POST /mis-compras y POST /mis-compras/procesar-carrito no lo estan; este bypass debe considerarse un hallazgo de seguridad y no solo una nota de flujo.
  • La aprobacion de cupo no exige una secuencia estricta de todas las fases. El backend actual valida que existan al menos >= 5 tipos de proceso cuyo ultimo evento del mes sea FINISH_SUCCESS.
  • El flujo de registro aliado no queda realmente garantizado por el link firmado: solo el GET /aliados/postulacion/{postulacion}/registro usa signed; el POST /aliados/postulacion/registro no lo usa ni recibe {postulacion}.
  • Spatie Permission sigue siendo un componente instalado pero no gobierna la autorizacion efectiva observada en las rutas principales; la documentacion debe seguir priorizando rol y controles inline como mecanismo real.
  • Varias operaciones sensibles del portal aliado siguen dependiendo solo de auth:app sin restriccion adicional de rol, especialmente acciones administrativas y de postulacion.

1. Configuración de Guards

GuardDriverProviderModeloPor DefectoPropósito
websessionusersApp\Models\UserSí (env AUTH_GUARD)Marketplace orientado al cliente
appsessionusersApp\Models\UserNoPortal back-office aliado/socio

Ambos guards comparten el mismo provider (users) y el mismo modelo (App\Models\User). La diferenciación de usuarios se logra mediante una columna guard en la tabla users (string, asignada al momento de creación) y una columna rol (string, nullable, agregada en la migración 2026_01_11_000001).

Broker de Restablecimiento de Contraseña

BrokerProviderTablaExpiración del TokenThrottle
usersuserspassword_reset_tokens60 min60 seg

Timeout de confirmación de contraseña: 10 800 segundos (3 horas).

Timeout de verificación de correo: 60 minutos.


2. Registro de Aliases de Middleware

Registrados en bootstrap/app.php:

AliasClasePropósito
authApp\Http\Middleware\AuthenticateMiddleware de autenticación personalizado. Verifica los guards especificados; redirige a route('login') para el guard app o a route('home', ['login' => 'true']) para el guard web cuando no está autenticado.
roleSpatie\Permission\Middleware\RoleMiddlewareVerificación de rol de Spatie (registrado pero no usado en ninguna ruta)
permissionSpatie\Permission\Middleware\PermissionMiddlewareVerificación de permiso de Spatie (registrado pero no usado en ninguna ruta)
role_or_permissionSpatie\Permission\Middleware\RoleOrPermissionMiddlewareVerificación de rol-o-permiso de Spatie (registrado pero no usado en ninguna ruta)
cliente_registro_completoApp\Http\Middleware\EnsureClienteRegistroCompletoBloquea el acceso si el cliente no ha completado el registro o su cupo de crédito ha vencido; redirige a user.perfil.
consultar_cupo_clienteApp\Http\Middleware\ConsultarCupoDelClienteNo bloqueante. Consulta el servicio externo de crédito para refrescar cupo_disponible en el registro cliente. Falla silenciosamente.
verificar_cliente_presenta_moraApp\Http\Middleware\VerificarClientePresentaMoraBloquea el checkout si el cliente tiene pagos en mora (presentaMora); redirige al home con alerta. Falla en abierto ante excepción.
check_intentos_limite_diariosApp\Http\Middleware\CheckIntentosLimiteDiariosLanza ValidationException si el cliente ha excedido el límite diario de intentos de aprobación de crédito.

Stack Global de Middleware Web

Adjunto al grupo de middleware web (aplicado a todas las rutas web):

OrdenClasePropósito
1HandleAppearanceComparte el valor de la cookie appearance con todas las vistas Blade
2HandleInertiaRequestsDatos compartidos de Inertia SSR: usuario autenticado, rol, carrito, configuración de crédito, lineas, marcas, links
3AddLinkHeadersForPreloadedAssetsHints de HTTP/2 push para assets de Vite

Excepciones de Encriptación de Cookies

appearance, sidebar_state — estas cookies no son encriptadas.


3. Mapeo de Grupos de Rutas a Middleware y Guard

Grupo de RutasPrefijoStack de MiddlewareGuard
Marketplace público/grupo web (global)Ninguno (anónimo)
Página de inicio/grupo web + consultar_cupo_clienteNinguno (anónimo, pero refresca cupo si está logueado)
Auth invitado cliente/registro, /login, etc.guest (guard por defecto = web)web
Auth autenticado cliente/verificar-correo, /logout, etc.auth:webweb
Cliente autenticado/usuario/*, /mis-compras/*auth (guard por defecto = web)web
Perfil de cliente (verificado)/usuario/perfil, /usuario/completar-registroauth + verifiedweb
Flujo de aprobación de crédito (Fases 1-6)/usuario/cupo/legal-check, /identity-validation*, /hdc-validationauth + check_intentos_limite_diariosweb
Paso final de aprobación de crédito (Fase 7)/usuario/cupo/aprobarsolo auth (NO limitado por check_intentos_limite_diarios)web
Verificación de límite/usuario/cupo/verificar-limite-intentossolo authweb
Checkout/checkoutauth + cliente_registro_completo + verificar_cliente_presenta_moraweb
Configuración del cliente/settings/*auth:webweb
Auth invitado aliado/aliados/ingresar, /aliados/loginguest:appapp
Aliado autenticado/aliados/*auth:appapp
Registro de postulación aliado/aliados/postulacion/{id}/registroauth:app + signedapp
API/api/*grupo de middleware apiNinguno (stateless, sin auth)

4. Definiciones de Roles

Los roles se almacenan como un string plano en la columna users.rol. No se usa una tabla de roles a pesar de que Spatie Permission está instalado con teams: true.

Roles Descubiertos

Valor del RolGuardContextoCómo se Asigna
clientewebUsuario final / cliente del marketplacePor defecto para registros del guard web; inferido por el comando UpdateUserRoles cuando el usuario no tiene pivot de empresa/sucursal
aliadoappDueño del negocio aliado / socioAsignado durante el registro del aliado (AliadoAuth\RegisteredUserController@store)
empleadoappEmpleado de un negocio aliadoInferido por UpdateUserRoles cuando el usuario tiene sucursal_id en el pivot
administradorappSuper-admin de la plataforma (Mi Plante)Asignado en EmpresaSeeder; puede ser creado vía endpoint storeAdmin
administrador_comercialappAdmin comercial (Mi Plante)Asignado en EmpresaSeeder; puede ser creado vía endpoint storeAdmin
administrador_financieroappAdmin financiero (Mi Plante)Asignado en EmpresaSeeder; puede ser creado vía endpoint storeAdmin

Patrón de “Roles Globales”

Varios controladores verifican acceso elevado usando un patrón inline (no middleware):

$isGlobalRole = in_array($rol, ['administrador', 'administrador_comercial', 'administrador_financiero']);

Esto se utiliza en:

  • Aliado\DashboardController — los roles globales saltan el requisito de sucursal_id
  • Aliado\VentaController — los roles globales ven datos de todas las sucursales
  • Aliado\VentaReporteComercialControllerabort(403) estricto si no es un rol global

Estado de Spatie Permission

  • config/permission.php está completamente configurado con teams: true
  • Los aliases de middleware role, permission, role_or_permission están registrados
  • Ninguna ruta usa estos aliases de middleware
  • El modelo User NO usa los traits HasRoles o HasPermissions
  • No existen seeders de roles/permisos
  • Las tablas roles, permissions, y tablas pivot probablemente existen (de las migraciones de Spatie) pero no se usan
  • La autorización se hace cumplir enteramente a través de la columna string rol + verificaciones inline en controladores

5. Diagramas de Cadena de Middleware

5.1 Flujo de Aprobación de Crédito

El cliente visita /usuario/cupo/legal-check
|
v
[middleware global web]
HandleAppearance -> HandleInertiaRequests -> AddLinkHeadersForPreloadedAssets
|
v
[auth] (middleware Authenticate, guard por defecto = web)
- ¿Está el usuario logueado en el guard 'web'?
- NO -> redirigir a route('home', ['login' => 'true'])
- SÍ -> continuar
|
v
[check_intentos_limite_diarios] (CheckIntentosLimiteDiarios)
- Cargar auth()->user()->cliente
- ¿AprobarCupoService::haSuperadoLimiteIntentosDiarios(cliente_id)?
- SÍ -> lanzar ValidationException ("Ha superado el limite de intentos diarios")
- NO -> continuar
|
v
[AprobarCupoController@legalCheck]

5.2 Flujo de Checkout

El cliente visita /checkout
|
v
[middleware global web]
HandleAppearance -> HandleInertiaRequests -> AddLinkHeadersForPreloadedAssets
|
v
[auth] (middleware Authenticate, guard por defecto = web)
- ¿Está el usuario logueado en el guard 'web'?
- NO -> redirigir a route('home', ['login' => 'true'])
- SÍ -> continuar
|
v
[cliente_registro_completo] (EnsureClienteRegistroCompleto)
- ¿El usuario tiene un registro cliente?
- ¿Es registro_completado_en null? -> redirigir a user.perfil
- ¿Es cupo_vence_en null? -> redirigir a user.perfil
- ¿Está cupo_vence_en en el pasado? -> redirigir a user.perfil
- Todo OK -> continuar
|
v
[verificar_cliente_presenta_mora] (VerificarClientePresentaMora)
- ¿CreditoService::clientePresentaMora(cliente)?
- SÍ -> redirigir a home con mensaje de alerta
- Excepción -> fallar en abierto (continuar)
- NO -> continuar
|
v
[Verificación inline del carrito en el closure de ruta]
- ¿El usuario tiene items en carrito?
- NO -> redirigir a home
- SÍ -> renderizar Checkout/Index

5.3 Flujo del Portal Aliado

El usuario aliado visita /aliados/ (dashboard)
|
v
[middleware global web]
HandleAppearance -> HandleInertiaRequests -> AddLinkHeadersForPreloadedAssets
|
v
[auth:app] (middleware Authenticate, guard = app)
- ¿Está el usuario logueado en el guard 'app'?
- NO -> redirigir a route('login') (aliados/ingresar)
- SÍ -> continuar
|
v
[DashboardController@index]
- Lee user->rol() inline
- ¿isGlobalRole? -> puede ver todas las sucursales
- no global -> alcance a su sucursal
El aliado visita /aliados/postulacion/{id}/registro (desde link firmado por correo)
|
v
[middleware global web]
|
v
[auth:app] (debe estar logueado en el guard 'app')
|
v
[signed] (middleware ValidateSignature de Laravel)
- ¿Es válida la firma de la URL?
- NO -> 403
- SÍ -> continuar
|
v
[RegisteredUserController@index]

6. Diagrama de Arquitectura de Auth

graph TB
    subgraph "Guards (config/auth.php)"
        WEB["guard web<br/>driver: session<br/>provider: users"]
        APP["guard app<br/>driver: session<br/>provider: users"]
    end

    subgraph "Provider"
        UP["provider users<br/>driver: eloquent<br/>modelo: User"]
    end

    WEB --> UP
    APP --> UP

    subgraph "Modelo User (tabla única)"
        UM["tabla users<br/>guard: string<br/>rol: string nullable<br/>activo: boolean"]
    end

    UP --> UM

    subgraph "Roles (columna string, sin Spatie)"
        R_CLI["cliente"]
        R_ALI["aliado"]
        R_EMP["empleado"]
        R_ADM["administrador"]
        R_ADC["administrador_comercial"]
        R_ADF["administrador_financiero"]
    end

    UM --> R_CLI
    UM --> R_ALI
    UM --> R_EMP
    UM --> R_ADM
    UM --> R_ADC
    UM --> R_ADF

    subgraph "Grupos de Rutas"
        PUB["Rutas Públicas<br/>(sin auth)"]
        CG["Invitado Cliente<br/>middleware: guest"]
        CA["Cliente Auth<br/>middleware: auth (web)"]
        AG["Invitado Aliado<br/>middleware: guest:app"]
        AA["Aliado Auth<br/>middleware: auth:app"]
        API_R["Rutas API<br/>middleware: api"]
    end

    subgraph "Middleware de Negocio"
        CC["consultar_cupo_cliente<br/>Refrescar cupo de crédito"]
        CRC["cliente_registro_completo<br/>Asegurar registro completo"]
        VCM["verificar_cliente_presenta_mora<br/>Bloquear si está en mora"]
        CIL["check_intentos_limite_diarios<br/>Limitar intentos de crédito"]
    end

    CA --> CC
    CA --> CRC
    CA --> VCM
    CA --> CIL

    subgraph "Autorización Inline (controladores)"
        IA["verificación isGlobalRole<br/>administrador | administrador_comercial | administrador_financiero"]
    end

    AA --> IA

    subgraph "Spatie Permission (instalado pero sin usar)"
        SP["middleware role<br/>middleware permission<br/>middleware role_or_permission"]
    end

    SP -. "registrado pero<br/>no aplicado a rutas" .-> AA
    SP -. "El modelo User carece<br/>del trait HasRoles" .-> UM

    subgraph "Redirecciones No Autenticadas"
        RH["route('home', login=true)<br/>para guard web"]
        RL["route('login')<br/>(aliados/ingresar)<br/>para guard app"]
    end

    WEB -. "no autenticado" .-> RH
    APP -. "no autenticado" .-> RL

    subgraph "Otros Controles de Acceso"
        LV["LogViewer<br/>solo rol === administrador"]
        SR["Rutas Firmadas<br/>/aliados/postulacion/{id}/registro"]
        EV["Verificación de Correo<br/>signed + throttle:6,1"]
    end

7. Observaciones de Seguridad

#HallazgoSeveridadDetalle
1Spatie Permission instalado pero no conectadoBajaEl paquete está configurado (teams: true, middleware registrado) pero el modelo User no usa HasRoles/HasPermissions. Las tablas de roles y permisos existen pero no se usan. Esto es peso muerto; o se integra o se remueve.
2La autorización es inline, no se hace cumplir vía middlewareMediaLas verificaciones de rol ocurren dentro de los controladores (in_array($rol, [...])) en lugar de vía middleware de ruta. Esto es frágil y fácil de olvidar en nuevos endpoints.
3Modelo User único, guards duales, mismo providerInfoAmbos guards web y app resuelven a la misma tabla users. Ambos controladores de login filtran activamente por la columna guard antes de la autenticación (LoginRequest filtra guard='web', AliadoAuth\AuthenticatedSessionController filtra guard='app'). El login cruzado entre guards se previene a nivel de controlador, pero el guard de sesión por sí mismo no hace cumplir este filtro — cualquier nuevo endpoint de login que omita el filtro permitiría autenticación cruzada entre guards.
13El login de aliado carece de rate limitingMediaEl login de cliente usa RateLimiter (5 intentos), pero AuthenticatedSessionController::login() del aliado no tiene rate limiting, haciéndolo susceptible a ataques de fuerza bruta.
14El endpoint de acción de postulación carece de autorización por rolMediaPOST /aliados/postulacion/{postulacion}/action/{action} permite a cualquier usuario auth:app aprobar, rechazar, o descartar postulaciones. Ninguna verificación inline de rol restringe esto a administradores.
15El login de aliado hace cumplir verificaciones de activo y estado de empresaInfoEl controlador de login de aliado verifica $user->activo y $firstEmpresa->estado === 'activo' antes de permitir la autenticación. Los usuarios desactivados o usuarios cuya empresa está inactiva son denegados al iniciar sesión. Este control no se replica en el flujo de login del cliente.
4verificar_cliente_presenta_mora falla en abiertoMediaSi el servicio externo de crédito lanza una excepción, el middleware la captura y deja pasar la solicitud (return $next($request)). Una caída del servicio permitiría a clientes en mora alcanzar el checkout.
5Sin clases PolicyInfoEl directorio app/Policies/ no existe. Toda la autorización se maneja a nivel de controlador. No hay gates de autorización a nivel de modelo definidos.
6Las rutas API no tienen autenticaciónMediaLos tres endpoints de API (error-logs, datacredito/historial, webhooks/certicamara) son accesibles públicamente con solo el grupo de middleware api (rate limiting, sesión stateless). El endpoint historial de DataCredito expone datos del historial crediticio sin autenticación. El endpoint historial de DataCredito (GET /api/v1/datacredito/historial) es particularmente preocupante ya que acepta parámetros de tipo de documento, número, y apellido y retorna datos del historial crediticio — efectivamente una consulta pública al buró de crédito. Considerar elevar la severidad a Alta.
7El endpoint de creación de admin carece de guard de rolMediaPOST /aliados/usuarios/admin/crear valida que rol esté in:administrador,administrador_comercial,administrador_financiero pero solo requiere auth:app — cualquier usuario aliado autenticado (incluyendo el rol empleado) puede crear cuentas admin.
8CRUD público en marcasMediaCRUD público en marcas (store, update, destroy son accesibles públicamente sin autenticación). Para lineas, solo index y show son funcionales — las rutas store/update/destroy están registradas pero el controlador carece de esos métodos.
9Las rutas lista-deseos y carrito carecen de middleware authMediaRoute::apiResource('lista-deseos', ...) y Route::apiResource('carrito', ...) están definidas fuera del grupo Route::middleware('auth') en web.php. Las solicitudes no autenticadas causan errores 500 (referencia a usuario null) en lugar de respuestas 401 apropiadas.
10EnsureClienteRegistroCompleto falla en abierto para usuarios sin clienteBajaSi un usuario autenticado no tiene relación cliente, el middleware pasa sin bloquear, permitiendo potencialmente el acceso al checkout.
11POST /consultar-cupo accesible públicamenteBajaConsultarCupoController::store es accesible públicamente y debería examinarse por exposición de datos crediticios.
12El restablecimiento de contraseña no filtra por guard a nivel de brokerInfoEl flujo de restablecimiento de contraseña usa Password::sendResetLink() que consulta usuarios por correo sin filtrar por la columna guard. Un cliente podría disparar un restablecimiento para un correo de aliado.