Login y sesiones
Login, sesiones, seguridad y roles
Section titled “Login, sesiones, seguridad y roles”Esta pagina documenta el flujo completo de autenticacion del MedSync: desde que el usuario escribe telefono y contrasena hasta que se cierra la sesion, se invalida el refresh token o se bloquea el acceso por rol, permiso, tenant o sesion inactiva.
La regla mental mas importante es esta:
El backend genera un JWT firmado y el frontend lo guarda en la sesion del navegador. La base de datos guarda usuarios, sesiones activas y refresh tokens. En cada request protegido, el JWT viaja en el header Authorization.
Resumen visual
Section titled “Resumen visual”POST /api/auth/login con credenciales y bandera refresh.session y crea/renueva refresh_token.id_session.Archivos principales
Section titled “Archivos principales”| Area | Archivo | Responsabilidad |
|---|---|---|
| Vista login | hemia-assistance-front-legacy/src/views/Auth/Login.vue | Formulario de login, recuperacion de contrasena, checkbox “Mantener la sesion iniciada” y redireccion post-login. |
| Modal re-login | hemia-assistance-front-legacy/src/views/Auth/ReLogin.vue | Modal persistente para volver a autenticar cuando el layout detecta estado no autorizado. |
| Modelo login | hemia-assistance-front-legacy/src/models/auth/DAAuthModel.js | Payload que envia param, pwd, secretkey, timestamp, signature y refresh. |
| Servicio frontend | hemia-assistance-front-legacy/src/services/core/auth/DAAuthService.js | Llama login, close, refreshToken, sendOtp, validateOtp y resetPassword. |
| Store auth | hemia-assistance-front-legacy/src/store/modules/auth/actions.js | Orquesta login, refresh token, recuperacion de token y logout. |
| Mutaciones auth | hemia-assistance-front-legacy/src/store/modules/auth/mutations.js | Decodifica JWT, guarda token, negocio, refresh token y permisos en VueSession. |
| Router frontend | hemia-assistance-front-legacy/src/router/index.js | Protege rutas por requireAuth, rol y permiso. Redirige super admin a Super Admin Dashboard. |
| Cliente HTTP | hemia-assistance-front-legacy/src/services/config/core.instance.js | Agrega el header Authorization desde la sesion del frontend. |
| Rutas auth backend | hemia-assistance-back-legacy/src/routes/mysql/auth.js | Expone endpoints /login, /close, /forze-close, /refreshToken, OTP y reset password. |
| Controller auth | hemia-assistance-back-legacy/src/controller/mysql/auth.controller.js | Recibe request, delega a DAO y responde HTTP. |
| DAO auth | hemia-assistance-back-legacy/src/Dao/mysql/auth.dao.js | Valida usuario/password, arma payload, emite token, valida tenant, refresca token y cierra sesiones. |
| JWT middleware | hemia-assistance-back-legacy/src/auth/authenticate.js | Genera JWT y valida header Authorization en rutas protegidas. |
| Seguridad config | hemia-assistance-back-legacy/src/config/security.js | Lee JWT_EXPIRES_IN, JWT_SECRET, REQUIRE_ACTIVE_SESSION, CORS y valida startup. |
| Config JWT | hemia-assistance-back-legacy/src/config/jwt.js | Define llave JWT y expiracion usada por jsonwebtoken. |
| Sesion DAO | hemia-assistance-back-legacy/src/Dao/mysql/session.Dao.js | Busca y valida sesiones activas con active = 1 y last = 1. |
| Active session middleware | hemia-assistance-back-legacy/src/middleware/awaitHandlerFactory.middleware.js | Si REQUIRE_ACTIVE_SESSION=true, valida id_login + id_session contra BD en cada request protegido. |
| Rol middleware | hemia-assistance-back-legacy/src/middleware/requireRole.middleware.js | Bloquea endpoints si req.tokenDecode.rol no coincide con el rol requerido. |
| Tenant middleware | hemia-assistance-back-legacy/src/middleware/requireTenantOperational.middleware.js | Bloquea tenants inactivos, trials vencidos, pagos pendientes, suspendidos o cancelados. |
Endpoints de autenticacion
Section titled “Endpoints de autenticacion”| Endpoint | Protegido con JWT | Payload principal | Que hace |
|---|---|---|---|
POST /api/auth/login | No | param, pwd, refresh, secretkey, timestamp, signature | Valida usuario activo por telefono/email, compara password, crea sesion, genera JWT y devuelve permisos. |
PUT /api/auth/close | Si, Token.Auth | Sin payload obligatorio | Cierra la sesion del usuario autenticado y apaga su refresh token activo. |
PUT /api/auth/forze-close | Si, Token.Auth | userClose | Cierra de forma forzada la sesion activa de otro usuario. |
POST /api/auth/refreshToken | No | id_refresh_token o refresh_token | Valida refresh token activo y emite un nuevo access token. |
POST /api/auth/sendOtp | No | phone | Genera OTP de recuperacion y lo envia por correo. |
POST /api/auth/validateOtp | No | telephone, code | Valida el OTP. |
PUT /api/auth/resetPassword | No | phone, otp, password | Encripta y actualiza la contrasena. |
Flujo detallado de login
Section titled “Flujo detallado de login”- El usuario entra a
/auth/login. Login.vuemuestra telefono, contrasena y el checkboxMantener la sesion iniciada.- Al pulsar
ENTRAR,onLogin()valida campos requeridos. - El store arma un
DAAuthModelcon:param: telefono o email capturado.pwd: contrasena capturada.secretkey:process.env.VUE_APP_SECRET_KEY.timestamp: valor degetTimestamp().signature:toSHA256(secretkey + timestamp).refresh: valor del checkbox.
DAAuthService.login()llamaPOST /api/auth/login.AuthController.login()delega aAuthDao.login(body).AuthDao.login()llamaSP_login_by_telephone(:_param).- El procedimiento busca un usuario activo en
loginporemailotelephone. - Si existe, el backend compara
body.pwdcontralogin.passwordusandobcrypt.compare. - Si el password coincide, el DAO llama
SP_login_by_idlogin(:_id_login, :_refresh). - El procedimiento:
- Borra refresh tokens activos previos del usuario.
- Crea un nuevo refresh token si
_refresh = 1. - Marca sesiones activas previas como
active = 0ylast = 0. - Inserta una nueva fila en
sessionconactive = 1ylast = 1.
- El DAO consulta la sesion activa con
SessionDao.getActiveSession(id_login). - El DAO arma el payload del JWT con datos del usuario, rol, negocio e
id_session. - El DAO consulta permisos activos del rol desde
permission_rol+cat_permission. - Si el usuario no es
super_admin, valida que el tenant este operativo. GetAccessToken()firma el JWT conHS256.- La respuesta devuelve
token,refresh_tokensi aplica ypermisos. - El frontend guarda token, refresh token, negocio y permisos en
VueSession. Login.vueredirige:rol === 'super_admin'->/super-admin/dashboard- cualquier otro rol ->
/dashboard
flowchart TD A[Usuario abre /auth/login] --> B[Captura telefono y password] B --> C{Mantener sesion iniciada?} C --> D[POST /api/auth/login] D --> E[SP_login_by_telephone] E --> F{Usuario activo existe?} F -- No --> G[Error Usuario no encontrado] F -- Si --> H[bcrypt.compare password] H --> I{Password correcto?} I -- No --> J[Error password] I -- Si --> K[SP_login_by_idlogin] K --> L[Invalida refresh/sesiones anteriores] L --> M[Crea session activa last=1] M --> N[Arma payload con rol, negocio, id_session] N --> O[Valida tenant si no es super_admin] O --> P[Firma JWT HS256] P --> Q[Devuelve token, refresh_token, permisos] Q --> R[Frontend guarda en VueSession] R --> S[Redirige por rol]Payload del login
Section titled “Payload del login”Ejemplo conceptual del payload enviado por el frontend:
{ "param": "9610000000", "pwd": "********", "secretkey": "VUE_APP_SECRET_KEY", "timestamp": "valor-generado-en-front", "signature": "sha256(secretkey + timestamp)", "refresh": true}En el codigo actual, secretkey, timestamp y signature se envian desde el frontend, pero la validacion real observada en AuthDao.login() se centra en usuario activo y password con bcrypt. No se encontro en ese flujo una validacion server-side de la firma del payload.
Respuesta del login
Section titled “Respuesta del login”{ "token": "jwt.access.token", "refresh_token": "uuid-refresh-token-o-null", "permisos": ["permission.code"]}| Campo | Donde se usa | Nota |
|---|---|---|
token | VueSession.token, router, requests HTTP | Es el access token JWT. No se guarda en BD. |
refresh_token | VueSession.refreshtoken | Solo se guarda en frontend si el backend lo devuelve. Sirve para renovar el access token. |
permisos | VueSession.permissions, router y helpers de permisos | Lista de codigos desde cat_permission.code. |
Que contiene el JWT
Section titled “Que contiene el JWT”El payload del access token se arma en buildAuthPayload():
| Campo JWT | Origen | Uso |
|---|---|---|
email | login.email | Identidad visible del usuario. |
name | login.name | Nombre para UI/layout. |
telephone | login.telephone | Telefono de cuenta. |
id_login | login.id_login | Identificador usado en auditoria, sesiones y operaciones. |
id_rol | login.id_rol | Relacion con tabla rol. |
rol | rol.code | Autorizacion por rol, por ejemplo super_admin. |
id_business | login.id_business | Tenant asociado. Puede ser null para Super Admin. |
id_session | session.id_session activa | Permite validar la sesion viva en BD cuando REQUIRE_ACTIVE_SESSION=true. |
business_name | business.name | Nombre del tenant para UI/contexto. |
business_slug | business.slug | Slug del tenant. |
business_status | business.status | Estado operativo general del negocio. |
check | Agregado por GetAccessToken() | Bandera interna agregada antes de firmar. |
iat / exp | jsonwebtoken | Emision y expiracion del JWT cuando se firma con vigencia. |
Donde se guarda cada cosa
Section titled “Donde se guarda cada cosa”VueSession como token. Viaja en el header Authorization. No hay tabla de access tokens.refresh_token.id_refresh_token y en cliente como refreshtoken si el usuario eligio mantener sesion.session. El par active = 1 y last = 1 representa la sesion vigente.Tablas de base de datos
Section titled “Tablas de base de datos”| Campo | Tipo | Para que sirve en login |
|---|---|---|
id_login | varchar(50) | ID principal del usuario. Entra al JWT y a relaciones con sesiones, refresh tokens y auditoria. |
id_business | varchar(50) | Tenant asociado al usuario. Se agrega al JWT. |
id_rol | varchar(50) | Relacion con rol; define el rol tecnico. |
name | varchar(500) | Nombre visible. |
email | varchar(500) | Puede usarse para buscar usuario en login. |
telephone | varchar(50) | Puede usarse para buscar usuario en login. |
password | varchar(500) | Hash de contrasena comparado con bcrypt. No debe exponerse. |
locked | tinyint(1) | Campo disponible para bloqueo, no se observo validacion directa en SP_login_by_telephone. |
active | tinyint(1) | Debe estar en 1 para poder autenticar. |
created_at, created_by, updated_at, updated_by | auditoria | Trazabilidad del registro. |
session
Section titled “session”| Campo | Tipo | Para que sirve |
|---|---|---|
id_session | varchar(50) | ID de la sesion. Se inserta al iniciar sesion y se coloca dentro del JWT. |
id_login | varchar(50) | Usuario propietario de la sesion. |
last | tinyint(1) | Marca la sesion vigente principal. |
active | tinyint(1) | Marca si la sesion sigue activa. |
sec | int | Secuencia historica de sesiones por usuario. |
forze_by | varchar(50) | Usuario que cerro o forzo cierre. |
forze_at | datetime | Fecha de cierre forzado/cierre administrativo. |
created_at, created_by, updated_by, updated_at | auditoria | Trazabilidad de alta/cambio. |
La validacion fuerte de sesion activa busca exactamente:
SELECT id_session FROM `session` WHERE id_login = :_id_login AND id_session = :_id_session AND active = 1 AND last = 1 LIMIT 1;refresh_token
Section titled “refresh_token”| Campo | Tipo | Para que sirve |
|---|---|---|
id_refresh_token | varchar(50) | Token persistente generado como UUID por el procedimiento. |
id_login | varchar(50) | Usuario propietario. |
last | tinyint(1) | Solo tokens con last = 1 son aceptados para refresh. |
created_at, created_by | auditoria | Trazabilidad de creacion. |
| Campo | Tipo | Para que sirve |
|---|---|---|
id_rol | varchar(50) | ID de rol asociado a login.id_rol. |
desc_rol | varchar(100) | Nombre descriptivo. |
code | varchar(50) | Codigo usado en JWT y validaciones, por ejemplo super_admin. |
active | tinyint(1) | Estado del rol. |
permission_rol y cat_permission
Section titled “permission_rol y cat_permission”| Tabla | Campo clave | Para que sirve |
|---|---|---|
cat_permission | code | Codigo funcional del permiso. El frontend lo usa para permitir u ocultar rutas/acciones. |
cat_permission | active, is_active | Estado del permiso. |
permission_rol | id_rol, id_cat_permission | Relaciona roles con permisos. |
permission_rol | active | Solo permisos activos se devuelven al usuario. |
Consulta usada por el backend para permisos:
SELECT cp.code FROM permission_rol pr INNER JOIN cat_permission cp ON cp.id_cat_permission = pr.id_cat_permission AND cp.active = 1 WHERE pr.id_rol = :_id_rol AND pr.active = 1 ORDER BY cp.value, cp.sec;Mantener la sesion iniciada
Section titled “Mantener la sesion iniciada”El checkbox Mantener la sesion iniciada vive en Login.vue y ReLogin.vue. Su valor viaja como refresh dentro del payload de login.
| Checkbox | Frontend | Backend | Resultado practico |
|---|---|---|---|
| Marcado | auth.refresh = true | DAO calcula keepSession = true y devuelve refresh_token. | El frontend guarda refreshtoken; si el access token expira, puede llamar /api/auth/refreshToken. |
| No marcado | auth.refresh = false | DAO calcula keepSession = false y responde refresh_token: null. | El frontend no puede refrescar. Si el access token expira, el usuario debe iniciar sesion otra vez. |
En AuthDao.login(), el procedimiento SP_login_by_idlogin se invoca actualmente con _refresh : true siempre. Eso hace que el procedimiento cree un refresh token activo en BD incluso si el usuario no marco el checkbox. La diferencia real es que el DAO solo devuelve ese token al frontend cuando body.refresh === true.
Que pasa si dejo la sesion sin usar
Section titled “Que pasa si dejo la sesion sin usar”Hay dos conceptos distintos:
| Concepto | Donde vive | Que lo controla | Efecto |
|---|---|---|---|
| Expiracion del access token | JWT | JWT_EXPIRES_IN | Cuando vence, Token.Auth responde 401 aunque la fila de session siga activa. |
| Sesion activa en BD | Tabla session | active, last, cierres y REQUIRE_ACTIVE_SESSION | Si esta apagada y REQUIRE_ACTIVE_SESSION=true, el siguiente request protegido responde 401 aunque el JWT no haya vencido. |
| Refresh token | Tabla refresh_token + VueSession.refreshtoken | last = 1 y checkbox mantener sesion | Permite pedir un nuevo access token sin capturar password. |
Escenarios comunes:
/api/auth/refreshToken usando refreshtoken y recibir otro JWT.REQUIRE_ACTIVE_SESSION=true, el siguiente request protegido falla porque session.active y session.last ya no estan en 1.refresh_token.last = 0, /api/auth/refreshToken no encuentra token valido y obliga a iniciar sesion otra vez.Duracion y configuracion
Section titled “Duracion y configuracion”| Variable | Archivo | Valor/Regla | Impacto |
|---|---|---|---|
JWT_SECRET | src/config/security.js, src/config/jwt.js | Si no existe usa __DEFAULT_INSECURE_JWT_SECRET_CHANGE_ME__. En produccion-like no debe faltar ni ser debil. | Firma y verifica JWT. Cambiarlo invalida tokens emitidos con la llave anterior. |
JWT_EXPIRES_IN | src/config/security.js, src/config/jwt.js | Default codigo: 60000. En pruebas controladas se ha usado 3600. Puede ser numero o formato aceptado por jsonwebtoken. | Define exp del access token. |
REQUIRE_ACTIVE_SESSION | src/config/security.js, awaitHandlerFactory.middleware.js | Debe ser string exacto true para activar validacion contra tabla session. | Si esta activo, un cierre forzado invalida el acceso en el siguiente request. |
CORS_ORIGIN | src/config/security.js | * por defecto. En produccion-like debe ser explicito. | Control de origenes permitidos. |
APP_ENV, ENVIRONMENT, NODE_ENV | src/config/security.js | Define si el entorno se considera production-like. | En produccion se rechaza startup con configuracion insegura. |
Para que el cierre de sesion sea efectivo inmediatamente, REQUIRE_ACTIVE_SESSION debe estar en true. Si esta deshabilitado, un access token ya emitido puede seguir funcionando hasta que expire.
Como viaja el token en rutas protegidas
Section titled “Como viaja el token en rutas protegidas”El frontend usa axios.helper.js para tomar el token de VueSession:
return token ? { headers: { Authorization: token } } : {};El backend lee authorization con getHeaders(req).authorization. Luego aplica:
const token = authorization && authorization.replace(/^Bearer\s+/i, '');Por eso el backend acepta tanto:
Authorization: jwt.access.tokencomo:
Authorization: Bearer jwt.access.tokenEl patron usado en rutas protegidas es:
router.get('/recurso', Token.Auth, awaitHandlerFactory(Controller.metodo));Para rutas Super Admin:
router.get('/dashboard/summary', Token.Auth, requireRole('super_admin'), awaitHandlerFactory(SuperAdminController.dashboardSummary));Para rutas tenant:
router.get('/list', Token.Auth, requireTenantOperational, awaitHandlerFactory(Controller.list));Capas de seguridad por request
Section titled “Capas de seguridad por request”| Capa | Archivo | Que valida | Error esperado |
|---|---|---|---|
Token.Auth | src/auth/authenticate.js | Existe header Authorization y JWT firma/expiracion validas. | 401 Token Invalido. |
awaitHandlerFactory | src/middleware/awaitHandlerFactory.middleware.js | Si REQUIRE_ACTIVE_SESSION=true, valida id_login + id_session activo en BD. | 401 UNAUTHORIZED |
requireRole('super_admin') | src/middleware/requireRole.middleware.js | req.tokenDecode.rol coincide con rol requerido. | 403 No tienes permiso... |
requireTenantOperational | src/middleware/requireTenantOperational.middleware.js | Tenant activo, trial vigente, suscripcion operativa. Super Admin pasa sin bloqueo tenant. | 403 con codigo operacional. |
| Router frontend | src/router/index.js | meta.requireAuth, meta.role, meta.permission. | Redireccion a login/dashboard. |
flowchart TD A[Request a endpoint protegido] --> B{Header Authorization existe?} B -- No --> C[401] B -- Si --> D{JWT valido y no expirado?} D -- No --> C D -- Si --> E[req.tokenDecode] E --> F{REQUIRE_ACTIVE_SESSION=true?} F -- No --> I[Continua] F -- Si --> G{session active=1 y last=1?} G -- No --> C G -- Si --> I I --> J{Endpoint exige rol?} J -- Si --> K{rol coincide?} K -- No --> L[403] K -- Si --> M[Controller] J -- No --> N{Endpoint tenant?} N -- Si --> O{tenant operativo?} O -- No --> L O -- Si --> M N -- No --> MRoles y permisos
Section titled “Roles y permisos”El sistema mezcla dos controles:
- Rol tecnico: viene de
rol.codey se copia al JWT comorol. - Permisos funcionales: vienen de
permission_rol+cat_permissiony se devuelven como arreglopermisos.
El rol decide accesos amplios:
| Uso | Ejemplo |
|---|---|
| Redireccion post-login | Si payload.rol === 'super_admin', ir a /super-admin/dashboard; si no, ir a /dashboard. |
| Rutas backend Super Admin | requireRole('super_admin'). |
| Rutas frontend por meta | meta.role en rutas protegidas. |
| Tenant operational | super_admin no se bloquea por tenant; roles tenant si. |
Permisos
Section titled “Permisos”Los permisos deciden acceso funcional mas fino:
| Uso | Ejemplo |
|---|---|
| Cargar permisos | AuthDao.getRolePermissions(id_rol). |
| Guardarlos en cliente | SET_AUTH_PERMISSIONS normaliza y guarda en VueSession. |
| Router frontend | Si una ruta tiene meta.permission, se valida con hasPermission(). |
| UI/acciones | Componentes pueden consultar permisos para mostrar u ocultar capacidades. |
Cierre de sesion normal
Section titled “Cierre de sesion normal”Cuando el usuario sale desde frontend:
logout()en el store revisa si haystate.token.- Si hay token, llama
DAAuthService.close(). DAAuthService.close()ejecutaPUT /api/auth/close.- La ruta usa
Token.Auth, por lo que el JWT debe seguir siendo valido para cerrar en backend. AuthController.close()tomareq.tokenDecode.id_login.AuthDao.close()llamaSP_Close_session(:_id_login).- El procedimiento:
- Marca
refresh_token.last = 0. - Marca
session.last = 0ysession.active = 0. - Escribe
forze_by = _id_loginyforze_at = NOW().
- Marca
- Frontend destruye
VueSession. - Frontend limpia token, refresh token, permisos y negocio del store.
- Redirige a
/.
UPDATE refresh_token rt SET last = 0 WHERE rt.id_login = _id_login AND rt.last = 1;
UPDATE session s SET s.last = 0, s.active = 0, s.forze_by = _id_login, s.forze_at = NOW() WHERE id_login = _id_login AND s.last = 1;Cierre forzado
Section titled “Cierre forzado”PUT /api/auth/forze-close permite cerrar la sesion activa de otro usuario:
| Dato | Descripcion |
|---|---|
| Usuario actor | Sale del JWT: req.tokenDecode.id_login. |
| Usuario afectado | Llega en body como userClose. |
| Refresh tokens afectados | refresh_token.last = 0 donde id_login = userClose. |
| Sesiones afectadas | session.active = 0 y last = 0 donde id_login = userClose. |
| Trazabilidad | session.forze_by = actor, session.forze_at = fecha. |
En Super Admin tambien existen endpoints especificos de sesiones:
PATCH /api/super-admin/sessions/:id/closePATCH /api/super-admin/users/:id/close-sessions
Esos endpoints se documentan en la seccion Sessions, y siguen la misma idea operacional: apagar sesion y refresh tokens para impedir renovacion.
Refresh token
Section titled “Refresh token”El refresh token se usa para pedir un nuevo access token sin volver a escribir password.
- El frontend guarda
refreshtokensolo si fue devuelto por login. - Cuando necesita renovar, llama
POST /api/auth/refreshToken. - El payload se arma con
DARefreshTokenModel:
{ "id_refresh_token": "uuid-refresh-token"}- El backend busca el refresh token activo:
SELECT id_refresh_token, id_login FROM refresh_token WHERE id_refresh_token = :_refresh_token AND `last` = 1 LIMIT 1;- Si existe, arma de nuevo payload, valida tenant, firma un JWT nuevo y devuelve:
{ "token": "nuevo.jwt.access.token", "refresh_token": "refresh-token-activo", "permisos": ["permission.code"]}- Si no existe o esta apagado, el frontend limpia sesion y manda al usuario a
/.
Recuperacion de contrasena
Section titled “Recuperacion de contrasena”El flujo de “Olvidaste tu contrasena?” no inicia sesion automaticamente. Es un flujo paralelo:
- Usuario captura telefono.
- Frontend llama
POST /api/auth/sendOtpcon{ "phone": "..." }. - Backend llama
SP_otp_Save(:_phone, :_date). - Backend envia correo con el codigo.
- Usuario captura OTP.
- Frontend llama
POST /api/auth/validateOtpcontelephoneycode. - Si es valido, usuario captura nueva contrasena.
- Frontend llama
PUT /api/auth/resetPassword. - Backend encripta la nueva contrasena con bcrypt y llama
SP_login_update_password. - Usuario vuelve al formulario de login.
Estados y errores comunes
Section titled “Estados y errores comunes”| Situacion | Donde ocurre | Resultado |
|---|---|---|
| Usuario no existe o no esta activo | SP_login_by_telephone | Error controlado “Usuario no encontrado”. |
| Password incorrecto | AuthDao.login() | Respuesta no exitosa de password. |
| JWT ausente | Token.Auth | 401. |
| JWT expirado | jsonwebtoken.verify | 401. |
| JWT valido pero sesion cerrada | awaitHandlerFactory con REQUIRE_ACTIVE_SESSION=true | 401 en el siguiente request protegido. |
| Usuario sin rol requerido | requireRole | 403. |
| Tenant sin negocio | validateTenantCanAuthenticate o requireTenantOperational | 403 TENANT_REQUIRED. |
| Trial vencido | isBusinessOperational | 403 TRIAL_EXPIRED. |
| Pago pendiente | isBusinessOperational | 403 SUBSCRIPTION_PAST_DUE. |
| Tenant suspendido | isBusinessOperational | 403 TENANT_SUSPENDED. |
| Tenant cancelado | isBusinessOperational | 403 TENANT_CANCELLED. |
| Refresh token ausente en frontend | getRefreshtoken() | Logout y redireccion. |
| Refresh token apagado en BD | /api/auth/refreshToken | No se emite nuevo JWT; frontend limpia sesion. |
Analisis de seguridad actual
Section titled “Analisis de seguridad actual”Esta lectura esta hecha desde una perspectiva de auditoria MedSync: que tan bien protege el sistema hoy, que riesgos quedan abiertos y que tan suficiente es para operar. No reemplaza una prueba de penetracion, pero si ayuda a entender donde estamos parados.
Veredicto ejecutivo
Section titled “Veredicto ejecutivo”El sistema ya tiene piezas importantes: JWT firmado, hash de contrasena con bcrypt, sesiones persistidas en BD, cierre de sesion, refresh token revocable, roles, permisos y validacion de tenant. Con JWT_SECRET fuerte y REQUIRE_ACTIVE_SESSION=true, el modelo es defendible para una operacion MedSync controlada.
Para entornos con auditorias formales, datos sensibles, clientes enterprise o cumplimiento estricto, faltan controles como rotacion robusta de refresh tokens, expiracion server-side de refresh, rate limiting, MFA, deteccion de anomalias, cookies HttpOnly/SameSite o politicas mas fuertes de sesiones concurrentes.
Fortalezas
Section titled “Fortalezas”| Fortaleza | Evidencia en el sistema | Por que importa |
|---|---|---|
| Password no se compara en texto plano | AuthDao.login() usa bcrypt.compare(body.pwd, result[0].password). | Reduce impacto si alguien ve el flujo de autenticacion; la BD debe guardar hash, no contrasena real. |
| Access token firmado | GetAccessToken() usa jsonwebtoken con HS256 y JWT_SECRET. | Evita que el cliente altere rol, negocio o id_login sin invalidar la firma. |
| Expiracion de access token | configjwt.expiresIn usa JWT_EXPIRES_IN. | Limita el tiempo de vida del access token robado. |
| Sesion persistida en BD | Tabla session guarda id_session, id_login, active, last. | Permite invalidar sesiones sin esperar a que expire el JWT. |
| Validacion activa opcional por request | awaitHandlerFactory valida id_login + id_session si REQUIRE_ACTIVE_SESSION=true. | Convierte el cierre de sesion en bloqueo casi inmediato. |
| Refresh token revocable | Tabla refresh_token usa last = 1/0. | Permite renovar access tokens y tambien apagar renovaciones al cerrar sesion. |
| Cierre normal y forzado | SP_Close_session, SP_forze_session y endpoints Super Admin de sesiones. | Da control operativo cuando una cuenta queda comprometida o un usuario sale de la organizacion. |
| Separacion por rol | requireRole('super_admin') y rol.code en JWT. | Protege endpoints globales como Super Admin. |
| Permisos granulares | permission_rol + cat_permission y validacion frontend por meta.permission. | Permite controlar funciones mas finas que el rol general. |
| Validacion de tenant operacional | requireTenantOperational e isBusinessOperational. | Bloquea tenants inactivos, trials vencidos, pagos pendientes, suspendidos o cancelados. |
| Configuracion insegura falla en produccion-like | validateStartupConfig() rechaza secretos debiles, CORS abierto o sesiones activas deshabilitadas en produccion. | Baja el riesgo de desplegar con defaults peligrosos. |
Riesgos y debilidades
Section titled “Riesgos y debilidades”| Riesgo | Severidad | Evidencia / comportamiento actual | Impacto |
|---|---|---|---|
JWT_SECRET default existe en codigo | Alta si llega a produccion | security.js define DEFAULT_JWT_SECRET. | Si se despliega sin secreto fuerte, los JWT pueden ser falsificables. La mitigacion actual depende de validateStartupConfig() y entorno bien configurado. |
REQUIRE_ACTIVE_SESSION=false debilita cierres | Alta | La validacion de BD solo ocurre si la variable es true. | Cerrar una sesion no corta access tokens ya emitidos hasta que expiren. |
Refresh token guardado en VueSession | Media/Alta | Frontend guarda refreshtoken en sesion persistente del navegador. | Si hay XSS, el atacante podria robar access token y refresh token. |
| Access token tambien accesible desde JS | Media/Alta | VueSession.token es usado por axios.helper.js. | XSS compromete la sesion completa del usuario. |
| Refresh token sin expiracion visible en tabla | Media | refresh_token tiene created_at, last, pero no expires_at. | Un refresh token activo puede vivir hasta que se cierre o sea reemplazado. |
| Refresh token no se rota estrictamente en refresh | Media | /refreshToken devuelve el refresh activo; no se observa emision de nuevo refresh por cada renovacion. | Si un refresh token se filtra, puede reutilizarse mientras last = 1. |
| Login crea refresh en BD aunque no se devuelva | Media | SP_login_by_idlogin se llama con _refresh : true; el DAO solo devuelve token si keepSession. | Hay artefactos persistentes no usados por el cliente. No rompe login, pero complica auditoria y reduce claridad del modelo. |
Header Authorization sin prefijo obligatorio | Baja/Media | Backend acepta token crudo o Bearer. Frontend envia token crudo. | Funciona, pero se aleja del estandar y puede confundir integraciones, gateways o herramientas de seguridad. |
Firma secretkey/timestamp/signature no validada en DAO | Media | El frontend la envia, pero el flujo observado no la valida server-side. | Da falsa sensacion de control criptografico adicional; el control real es password + JWT. |
| Falta rate limiting documentado | Alta ante ataque de fuerza bruta | No se observo limitador en ruta /api/auth/login. | Permite intentos repetidos contra password/OTP si no hay proteccion externa. |
Campo locked no parece bloquear login | Media | SP_login_by_telephone valida active = 1, no locked. | Usuarios bloqueados podrian autenticarse si siguen activos, segun datos reales. |
| OTP sin politica visible en esta documentacion | Media | Se usan SP de OTP, pero no se ve aqui TTL/intentos maximos. | Riesgo de abuso de recuperacion de contrasena si BD/SP no limita expiracion e intentos. |
| Permisos se validan principalmente en frontend para rutas | Media | router/index.js valida meta.permission; backend usa roles/tenant en muchas rutas. | Ocultar UI no basta como control de seguridad si endpoints no verifican permiso especifico. |
| Sin MFA | Media/Alta para cuentas admin | Login actual es password + opcional refresh. | Una contrasena robada puede bastar para entrar, especialmente en Super Admin. |
| Sin monitoreo de anomalias documentado | Media | No se observo alertado por geografia, IP, dispositivo, velocidad o patrones. | Compromisos pueden tardar mas en detectarse. |
Evaluacion por capa
Section titled “Evaluacion por capa”| Capa | Estado actual | Madurez | Comentario auditor |
|---|---|---|---|
| Identidad | Usuario por telefono/email + password bcrypt | Media | Correcto como base. Falta politica fuerte de bloqueo, intentos y MFA. |
| Access token | JWT firmado con expiracion | Media/Alta | Bueno si JWT_SECRET es fuerte y JWT_EXPIRES_IN corto/razonable. |
| Sesion server-side | Tabla session + REQUIRE_ACTIVE_SESSION | Alta si esta habilitado | Es una fortaleza grande: permite invalidacion server-side de JWT. |
| Refresh token | UUID en BD + last | Media | Revocable, pero faltan expiracion, rotacion y deteccion de reuso. |
| Autorizacion | Rol + permisos + tenant operational | Media/Alta | La base es buena. Conviene mover mas permisos criticos al backend. |
| Frontend storage | VueSession persistente | Media/Baja | Practico, pero mas expuesto a XSS que cookies HttpOnly. |
| Operacion MedSync | Cierre forzado, Super Admin sessions | Alta | Buen control administrativo para respuesta a incidentes. |
| Hardening produccion | validateStartupConfig() | Alta | Muy buena decision: impide arrancar con defaults peligrosos en produccion-like. |
| Observabilidad | Parcial | Media/Baja | Hay auditoria en Super Admin, pero faltan senales especificas de auth/anomalias. |
Cuando “basta” y cuando no
Section titled “Cuando “basta” y cuando no”| Contexto | La seguridad actual basta? | Condiciones minimas |
|---|---|---|
| QA / demo interna | Si | Datos no reales, usuarios controlados, secretos no compartidos. |
| Produccion controlada inicial | Si, con cuidado | JWT_SECRET fuerte, REQUIRE_ACTIVE_SESSION=true, JWT_EXPIRES_IN razonable, HTTPS, CORS cerrado y monitoreo basico. |
| MedSync con clientes reales y datos sensibles | Parcial | Ademas de lo anterior, conviene agregar rate limiting, MFA para admins, hardening de refresh token y logs de seguridad. |
| Enterprise / compliance / auditoria externa | No todavia | Se requiere MFA, politicas formales de sesion, rotacion de tokens, trazabilidad completa, controles anti abuso y pruebas de seguridad periodicas. |
Mejoras y migraciones futuras
Section titled “Mejoras y migraciones futuras”Esta hoja de ruta prioriza robustez sin romper el sistema de golpe. La idea es evolucionar desde “funciona y tiene controles base” hacia una postura MedSync mas madura.
Prioridad 0: configuracion obligatoria
Section titled “Prioridad 0: configuracion obligatoria”| Mejora | Impacto | Esfuerzo | Recomendacion |
|---|---|---|---|
Exigir JWT_SECRET fuerte en todos los ambientes compartidos | Alto | Bajo | Mantener validateStartupConfig() y documentar longitud minima, rotacion y custodia del secreto. |
Usar REQUIRE_ACTIVE_SESSION=true por defecto en ambientes reales | Alto | Bajo | Hacerlo obligatorio para staging/produccion. |
Definir JWT_EXPIRES_IN explicito | Medio/Alto | Bajo | Evitar defaults ambiguos. Usar ventanas cortas para access token. |
| Cerrar CORS a dominios reales | Alto | Bajo | Evitar CORS_ORIGIN=* fuera de desarrollo. |
Prioridad 1: endurecer login
Section titled “Prioridad 1: endurecer login”| Mejora | Que cambia | Beneficio |
|---|---|---|
| Rate limiting por IP + usuario | Limitar intentos en /api/auth/login, /sendOtp, /validateOtp, /refreshToken. | Reduce fuerza bruta, password spraying y abuso de OTP. |
| Bloqueo temporal por intentos fallidos | Usar o reforzar login.locked y/o tabla de intentos. | Protege cuentas ante ataques repetidos. |
| Mensajes de error menos distinguibles | Unificar errores de usuario inexistente y password incorrecto. | Reduce enumeracion de usuarios. |
Validar locked en SP_login_by_telephone | Agregar condicion a login. | Hace efectivo el bloqueo operativo. |
| Politica de password | Longitud minima mayor, complejidad razonable, historial opcional. | Reduce riesgo por passwords debiles. |
| MFA para roles sensibles | Aplicar a super_admin y admins tenant. | Mitiga robo de password. |
Prioridad 2: modernizar tokens y sesiones
Section titled “Prioridad 2: modernizar tokens y sesiones”| Mejora | Que cambia | Beneficio |
|---|---|---|
Refresh token con expires_at | Agregar expiracion server-side a refresh_token. | Evita refresh indefinidos. |
| Rotacion de refresh token en cada uso | Cada /refreshToken invalida el refresh anterior y emite uno nuevo. | Reduce ventana si un refresh token se filtra. |
| Deteccion de reuso de refresh token | Si llega un token ya usado, cerrar todas las sesiones del usuario. | Senal fuerte de compromiso. |
| Guardar hash del refresh token | Persistir hash, no token en texto claro. | Si la BD se filtra, no entrega refresh tokens reutilizables. |
| Modelo de dispositivos | Guardar dispositivo, user agent, IP aproximada, fecha de ultima actividad. | Permite listar y cerrar sesiones por dispositivo. |
| Politica de sesiones concurrentes | Permitir una sola sesion, varias controladas o maximo por rol. | Evita sesiones olvidadas y reduce superficie. |
| Idle timeout server-side | Actualizar last_seen_at y cerrar despues de inactividad real. | Controla el caso “deje mi sesion abierta sin hacer nada”. |
Prioridad 3: almacenamiento mas seguro en frontend
Section titled “Prioridad 3: almacenamiento mas seguro en frontend”| Opcion | Ventaja | Consideracion |
|---|---|---|
Cookies HttpOnly, Secure, SameSite para refresh token | JS no puede leer el refresh token, reduce impacto de XSS. | Requiere ajustar CORS, CSRF y flujo de refresh. |
| Access token solo en memoria | Menos persistencia si se roba storage. | Al refrescar pagina se necesita refresh silencioso con cookie. |
| CSRF token si se usan cookies | Protege operaciones con cookie automatica. | Necesario si auth migra a cookies. |
| CSP estricta | Reduce probabilidad e impacto de XSS. | Requiere revisar scripts inline, assets y dominios permitidos. |
Prioridad 4: autorizacion mas robusta
Section titled “Prioridad 4: autorizacion mas robusta”| Mejora | Beneficio |
|---|---|
| Validar permisos criticos en backend, no solo en router/UI. | Evita bypass llamando endpoint directo. |
Crear middleware requirePermission('code'). | Estandariza permisos por endpoint. |
| Separar roles globales, roles tenant y permisos clinicos. | Reduce confusiones entre super_admin, admin tenant y usuario operativo. |
| Versionar permisos por modulo. | Facilita auditoria y migraciones futuras. |
| Registrar cambios de permisos en auditoria. | Permite trazabilidad de privilegios. |
Prioridad 5: observabilidad y respuesta a incidentes
Section titled “Prioridad 5: observabilidad y respuesta a incidentes”| Mejora | Que observar |
|---|---|
| Log de eventos auth | Login exitoso/fallido, refresh exitoso/fallido, logout, cierre forzado, OTP enviado, OTP fallido. |
| Alertas por anomalia | Muchos fallos, login desde ubicacion nueva, refresh repetido, intento con usuario inexistente. |
| Panel de sesiones por usuario | Dispositivo, IP, inicio, ultima actividad, cierre. |
| Auditoria de Super Admin | Toda accion de cierre, cambio de rol, permisos y estado tenant. |
| Playbooks de incidente | Que hacer ante cuenta comprometida, token filtrado o acceso indebido. |
Arquitectura objetivo recomendada
Section titled “Arquitectura objetivo recomendada”flowchart TD A[Login con password + MFA opcional] --> B[Access token corto en memoria] B --> C[Refresh token en cookie HttpOnly Secure SameSite] C --> D[Refresh endpoint con rotacion] D --> E{Refresh reusado?} E -- Si --> F[Cerrar sesiones y alertar] E -- No --> G[Emitir nuevo access token y nuevo refresh] G --> H[Request protegido] H --> I[JWT valido] I --> J[Sesion activa + idle timeout] J --> K[Rol + permiso backend] K --> L[Tenant operational] L --> M[Controller]Roadmap sugerido
Section titled “Roadmap sugerido”| Fase | Objetivo | Entregables |
|---|---|---|
| Fase 1 | Cerrar riesgos de configuracion | JWT_SECRET fuerte obligatorio, REQUIRE_ACTIVE_SESSION=true, CORS cerrado, JWT_EXPIRES_IN definido. |
| Fase 2 | Proteger login contra abuso | Rate limiting, bloqueo temporal, validacion de locked, errores menos enumerables. |
| Fase 3 | Fortalecer refresh token | expires_at, hash en BD, rotacion, deteccion de reuso. |
| Fase 4 | Reducir exposicion frontend | Migrar refresh a cookie HttpOnly/Secure/SameSite, access token en memoria, CSP. |
| Fase 5 | Autorizacion enterprise | Middleware de permisos backend, auditoria de cambios de rol/permisos, matriz de permisos por modulo. |
| Fase 6 | Seguridad operativa | Alertas, dashboard de eventos auth, playbooks, pruebas de penetracion periodicas. |
Recomendacion final de auditor
Section titled “Recomendacion final de auditor”No hace falta reemplazar todo el sistema de autenticacion de inmediato. La base actual es aprovechable. El camino mas sano es endurecer configuracion, abuso de login y refresh tokens primero; despues migrar almacenamiento frontend y permisos backend. Para Super Admin, MFA y sesiones activas obligatorias deberian tratarse como requisito, no como mejora opcional.
Checklist operativo
Section titled “Checklist operativo”- Confirmar que
JWT_SECRETeste configurado y no sea default. - Confirmar que
JWT_EXPIRES_INtenga el tiempo esperado. - En produccion controlada, usar
REQUIRE_ACTIVE_SESSION=true. - Revisar que el usuario tenga
login.active = 1. - Revisar que
login.id_rolapunte a unrol.codecorrecto. - Revisar permisos activos en
permission_rolycat_permission. - Para usuarios tenant, revisar
login.id_businessy estado MedSync del negocio. - Si el usuario queda fuera aunque el JWT parezca vigente, revisar
session.active,session.lasteid_sessiondel JWT. - Si “mantener sesion” no renueva, revisar
VueSession.refreshtokenyrefresh_token.last. - Si un cierre forzado no surte efecto inmediato, revisar
REQUIRE_ACTIVE_SESSION.
Mapa rapido de diagnostico
Section titled “Mapa rapido de diagnostico”| Pregunta | Revisar |
|---|---|
| El usuario puede iniciar sesion? | login.active, telefono/email, hash en login.password, SP_login_by_telephone. |
| Se genero sesion nueva? | Tabla session, ultimo registro con active = 1, last = 1. |
| El JWT trae rol correcto? | Decodificar payload y revisar rol, id_rol, id_business, id_session. |
| El endpoint recibe token? | Header Authorization enviado por core.instance.js. |
| El endpoint rechaza con 401? | Expiracion JWT o sesion activa requerida. |
| El endpoint rechaza con 403? | Rol, permiso o tenant no operativo. |
| El usuario puede renovar? | VueSession.refreshtoken existe y refresh_token.last = 1. |
| El logout apago todo? | refresh_token.last = 0, session.active = 0, session.last = 0. |