07 - Mapa de Autenticación y Autorización
Validated Corrections
GET /checkoutsi esta protegido porcliente_registro_completoyverificar_cliente_presenta_mora, peroPOST /mis-comprasyPOST /mis-compras/procesar-carritono 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
>= 5tipos de proceso cuyo ultimo evento del mes seaFINISH_SUCCESS. - El flujo de registro aliado no queda realmente garantizado por el link firmado: solo el
GET /aliados/postulacion/{postulacion}/registrousasigned; elPOST /aliados/postulacion/registrono lo usa ni recibe{postulacion}. Spatie Permissionsigue siendo un componente instalado pero no gobierna la autorizacion efectiva observada en las rutas principales; la documentacion debe seguir priorizandoroly controles inline como mecanismo real.- Varias operaciones sensibles del portal aliado siguen dependiendo solo de
auth:appsin restriccion adicional de rol, especialmente acciones administrativas y de postulacion.
1. Configuración de Guards
| Guard | Driver | Provider | Modelo | Por Defecto | Propósito |
|---|---|---|---|---|---|
web | session | users | App\Models\User | Sí (env AUTH_GUARD) | Marketplace orientado al cliente |
app | session | users | App\Models\User | No | Portal 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
| Broker | Provider | Tabla | Expiración del Token | Throttle |
|---|---|---|---|---|
users | users | password_reset_tokens | 60 min | 60 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:
| Alias | Clase | Propósito |
|---|---|---|
auth | App\Http\Middleware\Authenticate | Middleware 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. |
role | Spatie\Permission\Middleware\RoleMiddleware | Verificación de rol de Spatie (registrado pero no usado en ninguna ruta) |
permission | Spatie\Permission\Middleware\PermissionMiddleware | Verificación de permiso de Spatie (registrado pero no usado en ninguna ruta) |
role_or_permission | Spatie\Permission\Middleware\RoleOrPermissionMiddleware | Verificación de rol-o-permiso de Spatie (registrado pero no usado en ninguna ruta) |
cliente_registro_completo | App\Http\Middleware\EnsureClienteRegistroCompleto | Bloquea el acceso si el cliente no ha completado el registro o su cupo de crédito ha vencido; redirige a user.perfil. |
consultar_cupo_cliente | App\Http\Middleware\ConsultarCupoDelCliente | No bloqueante. Consulta el servicio externo de crédito para refrescar cupo_disponible en el registro cliente. Falla silenciosamente. |
verificar_cliente_presenta_mora | App\Http\Middleware\VerificarClientePresentaMora | Bloquea el checkout si el cliente tiene pagos en mora (presentaMora); redirige al home con alerta. Falla en abierto ante excepción. |
check_intentos_limite_diarios | App\Http\Middleware\CheckIntentosLimiteDiarios | Lanza 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):
| Orden | Clase | Propósito |
|---|---|---|
| 1 | HandleAppearance | Comparte el valor de la cookie appearance con todas las vistas Blade |
| 2 | HandleInertiaRequests | Datos compartidos de Inertia SSR: usuario autenticado, rol, carrito, configuración de crédito, lineas, marcas, links |
| 3 | AddLinkHeadersForPreloadedAssets | Hints 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 Rutas | Prefijo | Stack de Middleware | Guard |
|---|---|---|---|
| Marketplace público | / | grupo web (global) | Ninguno (anónimo) |
| Página de inicio | / | grupo web + consultar_cupo_cliente | Ninguno (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:web | web |
| Cliente autenticado | /usuario/*, /mis-compras/* | auth (guard por defecto = web) | web |
| Perfil de cliente (verificado) | /usuario/perfil, /usuario/completar-registro | auth + verified | web |
| Flujo de aprobación de crédito (Fases 1-6) | /usuario/cupo/legal-check, /identity-validation*, /hdc-validation | auth + check_intentos_limite_diarios | web |
| Paso final de aprobación de crédito (Fase 7) | /usuario/cupo/aprobar | solo auth (NO limitado por check_intentos_limite_diarios) | web |
| Verificación de límite | /usuario/cupo/verificar-limite-intentos | solo auth | web |
| Checkout | /checkout | auth + cliente_registro_completo + verificar_cliente_presenta_mora | web |
| Configuración del cliente | /settings/* | auth:web | web |
| Auth invitado aliado | /aliados/ingresar, /aliados/login | guest:app | app |
| Aliado autenticado | /aliados/* | auth:app | app |
| Registro de postulación aliado | /aliados/postulacion/{id}/registro | auth:app + signed | app |
| API | /api/* | grupo de middleware api | Ninguno (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 Rol | Guard | Contexto | Cómo se Asigna |
|---|---|---|---|
cliente | web | Usuario final / cliente del marketplace | Por defecto para registros del guard web; inferido por el comando UpdateUserRoles cuando el usuario no tiene pivot de empresa/sucursal |
aliado | app | Dueño del negocio aliado / socio | Asignado durante el registro del aliado (AliadoAuth\RegisteredUserController@store) |
empleado | app | Empleado de un negocio aliado | Inferido por UpdateUserRoles cuando el usuario tiene sucursal_id en el pivot |
administrador | app | Super-admin de la plataforma (Mi Plante) | Asignado en EmpresaSeeder; puede ser creado vía endpoint storeAdmin |
administrador_comercial | app | Admin comercial (Mi Plante) | Asignado en EmpresaSeeder; puede ser creado vía endpoint storeAdmin |
administrador_financiero | app | Admin 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 desucursal_idAliado\VentaController— los roles globales ven datos de todas las sucursalesAliado\VentaReporteComercialController—abort(403)estricto si no es un rol global
Estado de Spatie Permission
config/permission.phpestá completamente configurado conteams: true- Los aliases de middleware
role,permission,role_or_permissionestán registrados - Ninguna ruta usa estos aliases de middleware
- El modelo
UserNO usa los traitsHasRolesoHasPermissions - 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/Index5.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 sucursal5.4 Link Firmado de Registro de Aliado
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
| # | Hallazgo | Severidad | Detalle |
|---|---|---|---|
| 1 | Spatie Permission instalado pero no conectado | Baja | El 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. |
| 2 | La autorización es inline, no se hace cumplir vía middleware | Media | Las 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. |
| 3 | Modelo User único, guards duales, mismo provider | Info | Ambos 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. |
| 13 | El login de aliado carece de rate limiting | Media | El login de cliente usa RateLimiter (5 intentos), pero AuthenticatedSessionController::login() del aliado no tiene rate limiting, haciéndolo susceptible a ataques de fuerza bruta. |
| 14 | El endpoint de acción de postulación carece de autorización por rol | Media | POST /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. |
| 15 | El login de aliado hace cumplir verificaciones de activo y estado de empresa | Info | El 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. |
| 4 | verificar_cliente_presenta_mora falla en abierto | Media | Si 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. |
| 5 | Sin clases Policy | Info | El 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. |
| 6 | Las rutas API no tienen autenticación | Media | Los 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. |
| 7 | El endpoint de creación de admin carece de guard de rol | Media | POST /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. |
| 8 | CRUD público en marcas | Media | CRUD 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. |
| 9 | Las rutas lista-deseos y carrito carecen de middleware auth | Media | Route::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. |
| 10 | EnsureClienteRegistroCompleto falla en abierto para usuarios sin cliente | Baja | Si un usuario autenticado no tiene relación cliente, el middleware pasa sin bloquear, permitiendo potencialmente el acceso al checkout. |
| 11 | POST /consultar-cupo accesible públicamente | Baja | ConsultarCupoController::store es accesible públicamente y debería examinarse por exposición de datos crediticios. |
| 12 | El restablecimiento de contraseña no filtra por guard a nivel de broker | Info | El 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. |