Skip to content
Usuario

Login y sesiones

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 access token no vive en la base de datos.

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.

1. Login UIEl usuario captura telefono, contrasena y opcionalmente marca Mantener la sesion iniciada.
2. Auth APIFrontend llama POST /api/auth/login con credenciales y bandera refresh.
3. BDBackend valida usuario, password, crea una fila nueva en session y crea/renueva refresh_token.
4. JWTBackend firma un access token con usuario, rol, negocio y id_session.
5. RutasLas rutas protegidas validan JWT, sesion activa, rol, permisos y estado operacional del tenant.
AreaArchivoResponsabilidad
Vista loginhemia-assistance-front-legacy/src/views/Auth/Login.vueFormulario de login, recuperacion de contrasena, checkbox “Mantener la sesion iniciada” y redireccion post-login.
Modal re-loginhemia-assistance-front-legacy/src/views/Auth/ReLogin.vueModal persistente para volver a autenticar cuando el layout detecta estado no autorizado.
Modelo loginhemia-assistance-front-legacy/src/models/auth/DAAuthModel.jsPayload que envia param, pwd, secretkey, timestamp, signature y refresh.
Servicio frontendhemia-assistance-front-legacy/src/services/core/auth/DAAuthService.jsLlama login, close, refreshToken, sendOtp, validateOtp y resetPassword.
Store authhemia-assistance-front-legacy/src/store/modules/auth/actions.jsOrquesta login, refresh token, recuperacion de token y logout.
Mutaciones authhemia-assistance-front-legacy/src/store/modules/auth/mutations.jsDecodifica JWT, guarda token, negocio, refresh token y permisos en VueSession.
Router frontendhemia-assistance-front-legacy/src/router/index.jsProtege rutas por requireAuth, rol y permiso. Redirige super admin a Super Admin Dashboard.
Cliente HTTPhemia-assistance-front-legacy/src/services/config/core.instance.jsAgrega el header Authorization desde la sesion del frontend.
Rutas auth backendhemia-assistance-back-legacy/src/routes/mysql/auth.jsExpone endpoints /login, /close, /forze-close, /refreshToken, OTP y reset password.
Controller authhemia-assistance-back-legacy/src/controller/mysql/auth.controller.jsRecibe request, delega a DAO y responde HTTP.
DAO authhemia-assistance-back-legacy/src/Dao/mysql/auth.dao.jsValida usuario/password, arma payload, emite token, valida tenant, refresca token y cierra sesiones.
JWT middlewarehemia-assistance-back-legacy/src/auth/authenticate.jsGenera JWT y valida header Authorization en rutas protegidas.
Seguridad confighemia-assistance-back-legacy/src/config/security.jsLee JWT_EXPIRES_IN, JWT_SECRET, REQUIRE_ACTIVE_SESSION, CORS y valida startup.
Config JWThemia-assistance-back-legacy/src/config/jwt.jsDefine llave JWT y expiracion usada por jsonwebtoken.
Sesion DAOhemia-assistance-back-legacy/src/Dao/mysql/session.Dao.jsBusca y valida sesiones activas con active = 1 y last = 1.
Active session middlewarehemia-assistance-back-legacy/src/middleware/awaitHandlerFactory.middleware.jsSi REQUIRE_ACTIVE_SESSION=true, valida id_login + id_session contra BD en cada request protegido.
Rol middlewarehemia-assistance-back-legacy/src/middleware/requireRole.middleware.jsBloquea endpoints si req.tokenDecode.rol no coincide con el rol requerido.
Tenant middlewarehemia-assistance-back-legacy/src/middleware/requireTenantOperational.middleware.jsBloquea tenants inactivos, trials vencidos, pagos pendientes, suspendidos o cancelados.
EndpointProtegido con JWTPayload principalQue hace
POST /api/auth/loginNoparam, pwd, refresh, secretkey, timestamp, signatureValida usuario activo por telefono/email, compara password, crea sesion, genera JWT y devuelve permisos.
PUT /api/auth/closeSi, Token.AuthSin payload obligatorioCierra la sesion del usuario autenticado y apaga su refresh token activo.
PUT /api/auth/forze-closeSi, Token.AuthuserCloseCierra de forma forzada la sesion activa de otro usuario.
POST /api/auth/refreshTokenNoid_refresh_token o refresh_tokenValida refresh token activo y emite un nuevo access token.
POST /api/auth/sendOtpNophoneGenera OTP de recuperacion y lo envia por correo.
POST /api/auth/validateOtpNotelephone, codeValida el OTP.
PUT /api/auth/resetPasswordNophone, otp, passwordEncripta y actualiza la contrasena.
  1. El usuario entra a /auth/login.
  2. Login.vue muestra telefono, contrasena y el checkbox Mantener la sesion iniciada.
  3. Al pulsar ENTRAR, onLogin() valida campos requeridos.
  4. El store arma un DAAuthModel con:
    • param: telefono o email capturado.
    • pwd: contrasena capturada.
    • secretkey: process.env.VUE_APP_SECRET_KEY.
    • timestamp: valor de getTimestamp().
    • signature: toSHA256(secretkey + timestamp).
    • refresh: valor del checkbox.
  5. DAAuthService.login() llama POST /api/auth/login.
  6. AuthController.login() delega a AuthDao.login(body).
  7. AuthDao.login() llama SP_login_by_telephone(:_param).
  8. El procedimiento busca un usuario activo en login por email o telephone.
  9. Si existe, el backend compara body.pwd contra login.password usando bcrypt.compare.
  10. Si el password coincide, el DAO llama SP_login_by_idlogin(:_id_login, :_refresh).
  11. El procedimiento:
    • Borra refresh tokens activos previos del usuario.
    • Crea un nuevo refresh token si _refresh = 1.
    • Marca sesiones activas previas como active = 0 y last = 0.
    • Inserta una nueva fila en session con active = 1 y last = 1.
  12. El DAO consulta la sesion activa con SessionDao.getActiveSession(id_login).
  13. El DAO arma el payload del JWT con datos del usuario, rol, negocio e id_session.
  14. El DAO consulta permisos activos del rol desde permission_rol + cat_permission.
  15. Si el usuario no es super_admin, valida que el tenant este operativo.
  16. GetAccessToken() firma el JWT con HS256.
  17. La respuesta devuelve token, refresh_token si aplica y permisos.
  18. El frontend guarda token, refresh token, negocio y permisos en VueSession.
  19. Login.vue redirige:
    • 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]

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.

{
"token": "jwt.access.token",
"refresh_token": "uuid-refresh-token-o-null",
"permisos": ["permission.code"]
}
CampoDonde se usaNota
tokenVueSession.token, router, requests HTTPEs el access token JWT. No se guarda en BD.
refresh_tokenVueSession.refreshtokenSolo se guarda en frontend si el backend lo devuelve. Sirve para renovar el access token.
permisosVueSession.permissions, router y helpers de permisosLista de codigos desde cat_permission.code.

El payload del access token se arma en buildAuthPayload():

Campo JWTOrigenUso
emaillogin.emailIdentidad visible del usuario.
namelogin.nameNombre para UI/layout.
telephonelogin.telephoneTelefono de cuenta.
id_loginlogin.id_loginIdentificador usado en auditoria, sesiones y operaciones.
id_rollogin.id_rolRelacion con tabla rol.
rolrol.codeAutorizacion por rol, por ejemplo super_admin.
id_businesslogin.id_businessTenant asociado. Puede ser null para Super Admin.
id_sessionsession.id_session activaPermite validar la sesion viva en BD cuando REQUIRE_ACTIVE_SESSION=true.
business_namebusiness.nameNombre del tenant para UI/contexto.
business_slugbusiness.slugSlug del tenant.
business_statusbusiness.statusEstado operativo general del negocio.
checkAgregado por GetAccessToken()Bandera interna agregada antes de firmar.
iat / expjsonwebtokenEmision y expiracion del JWT cuando se firma con vigencia.
Access tokenSe guarda del lado cliente en VueSession como token. Viaja en el header Authorization. No hay tabla de access tokens.
Refresh tokenSe guarda en BD en refresh_token.id_refresh_token y en cliente como refreshtoken si el usuario eligio mantener sesion.
Sesion activaSe guarda en la tabla session. El par active = 1 y last = 1 representa la sesion vigente.
CampoTipoPara que sirve en login
id_loginvarchar(50)ID principal del usuario. Entra al JWT y a relaciones con sesiones, refresh tokens y auditoria.
id_businessvarchar(50)Tenant asociado al usuario. Se agrega al JWT.
id_rolvarchar(50)Relacion con rol; define el rol tecnico.
namevarchar(500)Nombre visible.
emailvarchar(500)Puede usarse para buscar usuario en login.
telephonevarchar(50)Puede usarse para buscar usuario en login.
passwordvarchar(500)Hash de contrasena comparado con bcrypt. No debe exponerse.
lockedtinyint(1)Campo disponible para bloqueo, no se observo validacion directa en SP_login_by_telephone.
activetinyint(1)Debe estar en 1 para poder autenticar.
created_at, created_by, updated_at, updated_byauditoriaTrazabilidad del registro.
CampoTipoPara que sirve
id_sessionvarchar(50)ID de la sesion. Se inserta al iniciar sesion y se coloca dentro del JWT.
id_loginvarchar(50)Usuario propietario de la sesion.
lasttinyint(1)Marca la sesion vigente principal.
activetinyint(1)Marca si la sesion sigue activa.
secintSecuencia historica de sesiones por usuario.
forze_byvarchar(50)Usuario que cerro o forzo cierre.
forze_atdatetimeFecha de cierre forzado/cierre administrativo.
created_at, created_by, updated_by, updated_atauditoriaTrazabilidad 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;
CampoTipoPara que sirve
id_refresh_tokenvarchar(50)Token persistente generado como UUID por el procedimiento.
id_loginvarchar(50)Usuario propietario.
lasttinyint(1)Solo tokens con last = 1 son aceptados para refresh.
created_at, created_byauditoriaTrazabilidad de creacion.
CampoTipoPara que sirve
id_rolvarchar(50)ID de rol asociado a login.id_rol.
desc_rolvarchar(100)Nombre descriptivo.
codevarchar(50)Codigo usado en JWT y validaciones, por ejemplo super_admin.
activetinyint(1)Estado del rol.
TablaCampo clavePara que sirve
cat_permissioncodeCodigo funcional del permiso. El frontend lo usa para permitir u ocultar rutas/acciones.
cat_permissionactive, is_activeEstado del permiso.
permission_rolid_rol, id_cat_permissionRelaciona roles con permisos.
permission_rolactiveSolo 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;

El checkbox Mantener la sesion iniciada vive en Login.vue y ReLogin.vue. Su valor viaja como refresh dentro del payload de login.

CheckboxFrontendBackendResultado practico
Marcadoauth.refresh = trueDAO calcula keepSession = true y devuelve refresh_token.El frontend guarda refreshtoken; si el access token expira, puede llamar /api/auth/refreshToken.
No marcadoauth.refresh = falseDAO 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.
Comportamiento actual a conocer

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.

Hay dos conceptos distintos:

ConceptoDonde viveQue lo controlaEfecto
Expiracion del access tokenJWTJWT_EXPIRES_INCuando vence, Token.Auth responde 401 aunque la fila de session siga activa.
Sesion activa en BDTabla sessionactive, last, cierres y REQUIRE_ACTIVE_SESSIONSi esta apagada y REQUIRE_ACTIVE_SESSION=true, el siguiente request protegido responde 401 aunque el JWT no haya vencido.
Refresh tokenTabla refresh_token + VueSession.refreshtokenlast = 1 y checkbox mantener sesionPermite pedir un nuevo access token sin capturar password.

Escenarios comunes:

No marque mantener sesionCuando expire el JWT, el frontend no tiene refresh token guardado. Debe mostrar login o re-login.
Si marque mantener sesionCuando el JWT expire, el frontend puede llamar /api/auth/refreshToken usando refreshtoken y recibir otro JWT.
Sesion cerrada desde backendCon REQUIRE_ACTIVE_SESSION=true, el siguiente request protegido falla porque session.active y session.last ya no estan en 1.
Refresh apagadoSi refresh_token.last = 0, /api/auth/refreshToken no encuentra token valido y obliga a iniciar sesion otra vez.
VariableArchivoValor/ReglaImpacto
JWT_SECRETsrc/config/security.js, src/config/jwt.jsSi 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_INsrc/config/security.js, src/config/jwt.jsDefault codigo: 60000. En pruebas controladas se ha usado 3600. Puede ser numero o formato aceptado por jsonwebtoken.Define exp del access token.
REQUIRE_ACTIVE_SESSIONsrc/config/security.js, awaitHandlerFactory.middleware.jsDebe ser string exacto true para activar validacion contra tabla session.Si esta activo, un cierre forzado invalida el acceso en el siguiente request.
CORS_ORIGINsrc/config/security.js* por defecto. En produccion-like debe ser explicito.Control de origenes permitidos.
APP_ENV, ENVIRONMENT, NODE_ENVsrc/config/security.jsDefine si el entorno se considera production-like.En produccion se rechaza startup con configuracion insegura.
Produccion controlada

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.

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.token

como:

Authorization: Bearer jwt.access.token

El 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));
CapaArchivoQue validaError esperado
Token.Authsrc/auth/authenticate.jsExiste header Authorization y JWT firma/expiracion validas.401 Token Invalido.
awaitHandlerFactorysrc/middleware/awaitHandlerFactory.middleware.jsSi REQUIRE_ACTIVE_SESSION=true, valida id_login + id_session activo en BD.401 UNAUTHORIZED
requireRole('super_admin')src/middleware/requireRole.middleware.jsreq.tokenDecode.rol coincide con rol requerido.403 No tienes permiso...
requireTenantOperationalsrc/middleware/requireTenantOperational.middleware.jsTenant activo, trial vigente, suscripcion operativa. Super Admin pasa sin bloqueo tenant.403 con codigo operacional.
Router frontendsrc/router/index.jsmeta.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 --> M

El sistema mezcla dos controles:

  1. Rol tecnico: viene de rol.code y se copia al JWT como rol.
  2. Permisos funcionales: vienen de permission_rol + cat_permission y se devuelven como arreglo permisos.

El rol decide accesos amplios:

UsoEjemplo
Redireccion post-loginSi payload.rol === 'super_admin', ir a /super-admin/dashboard; si no, ir a /dashboard.
Rutas backend Super AdminrequireRole('super_admin').
Rutas frontend por metameta.role en rutas protegidas.
Tenant operationalsuper_admin no se bloquea por tenant; roles tenant si.

Los permisos deciden acceso funcional mas fino:

UsoEjemplo
Cargar permisosAuthDao.getRolePermissions(id_rol).
Guardarlos en clienteSET_AUTH_PERMISSIONS normaliza y guarda en VueSession.
Router frontendSi una ruta tiene meta.permission, se valida con hasPermission().
UI/accionesComponentes pueden consultar permisos para mostrar u ocultar capacidades.

Cuando el usuario sale desde frontend:

  1. logout() en el store revisa si hay state.token.
  2. Si hay token, llama DAAuthService.close().
  3. DAAuthService.close() ejecuta PUT /api/auth/close.
  4. La ruta usa Token.Auth, por lo que el JWT debe seguir siendo valido para cerrar en backend.
  5. AuthController.close() toma req.tokenDecode.id_login.
  6. AuthDao.close() llama SP_Close_session(:_id_login).
  7. El procedimiento:
    • Marca refresh_token.last = 0.
    • Marca session.last = 0 y session.active = 0.
    • Escribe forze_by = _id_login y forze_at = NOW().
  8. Frontend destruye VueSession.
  9. Frontend limpia token, refresh token, permisos y negocio del store.
  10. 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;

PUT /api/auth/forze-close permite cerrar la sesion activa de otro usuario:

DatoDescripcion
Usuario actorSale del JWT: req.tokenDecode.id_login.
Usuario afectadoLlega en body como userClose.
Refresh tokens afectadosrefresh_token.last = 0 donde id_login = userClose.
Sesiones afectadassession.active = 0 y last = 0 donde id_login = userClose.
Trazabilidadsession.forze_by = actor, session.forze_at = fecha.

En Super Admin tambien existen endpoints especificos de sesiones:

  • PATCH /api/super-admin/sessions/:id/close
  • PATCH /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.

El refresh token se usa para pedir un nuevo access token sin volver a escribir password.

  1. El frontend guarda refreshtoken solo si fue devuelto por login.
  2. Cuando necesita renovar, llama POST /api/auth/refreshToken.
  3. El payload se arma con DARefreshTokenModel:
{
"id_refresh_token": "uuid-refresh-token"
}
  1. 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;
  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"]
}
  1. Si no existe o esta apagado, el frontend limpia sesion y manda al usuario a /.

El flujo de “Olvidaste tu contrasena?” no inicia sesion automaticamente. Es un flujo paralelo:

  1. Usuario captura telefono.
  2. Frontend llama POST /api/auth/sendOtp con { "phone": "..." }.
  3. Backend llama SP_otp_Save(:_phone, :_date).
  4. Backend envia correo con el codigo.
  5. Usuario captura OTP.
  6. Frontend llama POST /api/auth/validateOtp con telephone y code.
  7. Si es valido, usuario captura nueva contrasena.
  8. Frontend llama PUT /api/auth/resetPassword.
  9. Backend encripta la nueva contrasena con bcrypt y llama SP_login_update_password.
  10. Usuario vuelve al formulario de login.
SituacionDonde ocurreResultado
Usuario no existe o no esta activoSP_login_by_telephoneError controlado “Usuario no encontrado”.
Password incorrectoAuthDao.login()Respuesta no exitosa de password.
JWT ausenteToken.Auth401.
JWT expiradojsonwebtoken.verify401.
JWT valido pero sesion cerradaawaitHandlerFactory con REQUIRE_ACTIVE_SESSION=true401 en el siguiente request protegido.
Usuario sin rol requeridorequireRole403.
Tenant sin negociovalidateTenantCanAuthenticate o requireTenantOperational403 TENANT_REQUIRED.
Trial vencidoisBusinessOperational403 TRIAL_EXPIRED.
Pago pendienteisBusinessOperational403 SUBSCRIPTION_PAST_DUE.
Tenant suspendidoisBusinessOperational403 TENANT_SUSPENDED.
Tenant canceladoisBusinessOperational403 TENANT_CANCELLED.
Refresh token ausente en frontendgetRefreshtoken()Logout y redireccion.
Refresh token apagado en BD/api/auth/refreshTokenNo se emite nuevo JWT; frontend limpia sesion.

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.

Base razonable para produccion controlada

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.

No es todavia una postura de seguridad madura de alta exigencia

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.

FortalezaEvidencia en el sistemaPor que importa
Password no se compara en texto planoAuthDao.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 firmadoGetAccessToken() usa jsonwebtoken con HS256 y JWT_SECRET.Evita que el cliente altere rol, negocio o id_login sin invalidar la firma.
Expiracion de access tokenconfigjwt.expiresIn usa JWT_EXPIRES_IN.Limita el tiempo de vida del access token robado.
Sesion persistida en BDTabla session guarda id_session, id_login, active, last.Permite invalidar sesiones sin esperar a que expire el JWT.
Validacion activa opcional por requestawaitHandlerFactory valida id_login + id_session si REQUIRE_ACTIVE_SESSION=true.Convierte el cierre de sesion en bloqueo casi inmediato.
Refresh token revocableTabla refresh_token usa last = 1/0.Permite renovar access tokens y tambien apagar renovaciones al cerrar sesion.
Cierre normal y forzadoSP_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 rolrequireRole('super_admin') y rol.code en JWT.Protege endpoints globales como Super Admin.
Permisos granularespermission_rol + cat_permission y validacion frontend por meta.permission.Permite controlar funciones mas finas que el rol general.
Validacion de tenant operacionalrequireTenantOperational e isBusinessOperational.Bloquea tenants inactivos, trials vencidos, pagos pendientes, suspendidos o cancelados.
Configuracion insegura falla en produccion-likevalidateStartupConfig() rechaza secretos debiles, CORS abierto o sesiones activas deshabilitadas en produccion.Baja el riesgo de desplegar con defaults peligrosos.
RiesgoSeveridadEvidencia / comportamiento actualImpacto
JWT_SECRET default existe en codigoAlta si llega a produccionsecurity.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 cierresAltaLa 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 VueSessionMedia/AltaFrontend guarda refreshtoken en sesion persistente del navegador.Si hay XSS, el atacante podria robar access token y refresh token.
Access token tambien accesible desde JSMedia/AltaVueSession.token es usado por axios.helper.js.XSS compromete la sesion completa del usuario.
Refresh token sin expiracion visible en tablaMediarefresh_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 refreshMedia/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 devuelvaMediaSP_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 obligatorioBaja/MediaBackend 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 DAOMediaEl 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 documentadoAlta ante ataque de fuerza brutaNo se observo limitador en ruta /api/auth/login.Permite intentos repetidos contra password/OTP si no hay proteccion externa.
Campo locked no parece bloquear loginMediaSP_login_by_telephone valida active = 1, no locked.Usuarios bloqueados podrian autenticarse si siguen activos, segun datos reales.
OTP sin politica visible en esta documentacionMediaSe 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 rutasMediarouter/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 MFAMedia/Alta para cuentas adminLogin actual es password + opcional refresh.Una contrasena robada puede bastar para entrar, especialmente en Super Admin.
Sin monitoreo de anomalias documentadoMediaNo se observo alertado por geografia, IP, dispositivo, velocidad o patrones.Compromisos pueden tardar mas en detectarse.
CapaEstado actualMadurezComentario auditor
IdentidadUsuario por telefono/email + password bcryptMediaCorrecto como base. Falta politica fuerte de bloqueo, intentos y MFA.
Access tokenJWT firmado con expiracionMedia/AltaBueno si JWT_SECRET es fuerte y JWT_EXPIRES_IN corto/razonable.
Sesion server-sideTabla session + REQUIRE_ACTIVE_SESSIONAlta si esta habilitadoEs una fortaleza grande: permite invalidacion server-side de JWT.
Refresh tokenUUID en BD + lastMediaRevocable, pero faltan expiracion, rotacion y deteccion de reuso.
AutorizacionRol + permisos + tenant operationalMedia/AltaLa base es buena. Conviene mover mas permisos criticos al backend.
Frontend storageVueSession persistenteMedia/BajaPractico, pero mas expuesto a XSS que cookies HttpOnly.
Operacion MedSyncCierre forzado, Super Admin sessionsAltaBuen control administrativo para respuesta a incidentes.
Hardening produccionvalidateStartupConfig()AltaMuy buena decision: impide arrancar con defaults peligrosos en produccion-like.
ObservabilidadParcialMedia/BajaHay auditoria en Super Admin, pero faltan senales especificas de auth/anomalias.
ContextoLa seguridad actual basta?Condiciones minimas
QA / demo internaSiDatos no reales, usuarios controlados, secretos no compartidos.
Produccion controlada inicialSi, con cuidadoJWT_SECRET fuerte, REQUIRE_ACTIVE_SESSION=true, JWT_EXPIRES_IN razonable, HTTPS, CORS cerrado y monitoreo basico.
MedSync con clientes reales y datos sensiblesParcialAdemas de lo anterior, conviene agregar rate limiting, MFA para admins, hardening de refresh token y logs de seguridad.
Enterprise / compliance / auditoria externaNo todaviaSe requiere MFA, politicas formales de sesion, rotacion de tokens, trazabilidad completa, controles anti abuso y pruebas de seguridad periodicas.

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.

MejoraImpactoEsfuerzoRecomendacion
Exigir JWT_SECRET fuerte en todos los ambientes compartidosAltoBajoMantener validateStartupConfig() y documentar longitud minima, rotacion y custodia del secreto.
Usar REQUIRE_ACTIVE_SESSION=true por defecto en ambientes realesAltoBajoHacerlo obligatorio para staging/produccion.
Definir JWT_EXPIRES_IN explicitoMedio/AltoBajoEvitar defaults ambiguos. Usar ventanas cortas para access token.
Cerrar CORS a dominios realesAltoBajoEvitar CORS_ORIGIN=* fuera de desarrollo.
MejoraQue cambiaBeneficio
Rate limiting por IP + usuarioLimitar intentos en /api/auth/login, /sendOtp, /validateOtp, /refreshToken.Reduce fuerza bruta, password spraying y abuso de OTP.
Bloqueo temporal por intentos fallidosUsar o reforzar login.locked y/o tabla de intentos.Protege cuentas ante ataques repetidos.
Mensajes de error menos distinguiblesUnificar errores de usuario inexistente y password incorrecto.Reduce enumeracion de usuarios.
Validar locked en SP_login_by_telephoneAgregar condicion a login.Hace efectivo el bloqueo operativo.
Politica de passwordLongitud minima mayor, complejidad razonable, historial opcional.Reduce riesgo por passwords debiles.
MFA para roles sensiblesAplicar a super_admin y admins tenant.Mitiga robo de password.
MejoraQue cambiaBeneficio
Refresh token con expires_atAgregar expiracion server-side a refresh_token.Evita refresh indefinidos.
Rotacion de refresh token en cada usoCada /refreshToken invalida el refresh anterior y emite uno nuevo.Reduce ventana si un refresh token se filtra.
Deteccion de reuso de refresh tokenSi llega un token ya usado, cerrar todas las sesiones del usuario.Senal fuerte de compromiso.
Guardar hash del refresh tokenPersistir hash, no token en texto claro.Si la BD se filtra, no entrega refresh tokens reutilizables.
Modelo de dispositivosGuardar dispositivo, user agent, IP aproximada, fecha de ultima actividad.Permite listar y cerrar sesiones por dispositivo.
Politica de sesiones concurrentesPermitir una sola sesion, varias controladas o maximo por rol.Evita sesiones olvidadas y reduce superficie.
Idle timeout server-sideActualizar 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”
OpcionVentajaConsideracion
Cookies HttpOnly, Secure, SameSite para refresh tokenJS no puede leer el refresh token, reduce impacto de XSS.Requiere ajustar CORS, CSRF y flujo de refresh.
Access token solo en memoriaMenos persistencia si se roba storage.Al refrescar pagina se necesita refresh silencioso con cookie.
CSRF token si se usan cookiesProtege operaciones con cookie automatica.Necesario si auth migra a cookies.
CSP estrictaReduce probabilidad e impacto de XSS.Requiere revisar scripts inline, assets y dominios permitidos.
MejoraBeneficio
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”
MejoraQue observar
Log de eventos authLogin exitoso/fallido, refresh exitoso/fallido, logout, cierre forzado, OTP enviado, OTP fallido.
Alertas por anomaliaMuchos fallos, login desde ubicacion nueva, refresh repetido, intento con usuario inexistente.
Panel de sesiones por usuarioDispositivo, IP, inicio, ultima actividad, cierre.
Auditoria de Super AdminToda accion de cierre, cambio de rol, permisos y estado tenant.
Playbooks de incidenteQue hacer ante cuenta comprometida, token filtrado o acceso indebido.
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]
FaseObjetivoEntregables
Fase 1Cerrar riesgos de configuracionJWT_SECRET fuerte obligatorio, REQUIRE_ACTIVE_SESSION=true, CORS cerrado, JWT_EXPIRES_IN definido.
Fase 2Proteger login contra abusoRate limiting, bloqueo temporal, validacion de locked, errores menos enumerables.
Fase 3Fortalecer refresh tokenexpires_at, hash en BD, rotacion, deteccion de reuso.
Fase 4Reducir exposicion frontendMigrar refresh a cookie HttpOnly/Secure/SameSite, access token en memoria, CSP.
Fase 5Autorizacion enterpriseMiddleware de permisos backend, auditoria de cambios de rol/permisos, matriz de permisos por modulo.
Fase 6Seguridad operativaAlertas, dashboard de eventos auth, playbooks, pruebas de penetracion periodicas.
Direccion recomendada

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.

  • Confirmar que JWT_SECRET este configurado y no sea default.
  • Confirmar que JWT_EXPIRES_IN tenga el tiempo esperado.
  • En produccion controlada, usar REQUIRE_ACTIVE_SESSION=true.
  • Revisar que el usuario tenga login.active = 1.
  • Revisar que login.id_rol apunte a un rol.code correcto.
  • Revisar permisos activos en permission_rol y cat_permission.
  • Para usuarios tenant, revisar login.id_business y estado MedSync del negocio.
  • Si el usuario queda fuera aunque el JWT parezca vigente, revisar session.active, session.last e id_session del JWT.
  • Si “mantener sesion” no renueva, revisar VueSession.refreshtoken y refresh_token.last.
  • Si un cierre forzado no surte efecto inmediato, revisar REQUIRE_ACTIVE_SESSION.
PreguntaRevisar
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.