Skip to content
Usuario

Login y sesiones

Esta pagina explica como debe funcionar hoy el login de MedSync con autenticacion basada en cookies. El objetivo es que cualquier persona de producto, soporte, QA, backend, frontend o infraestructura pueda entender que pasa cuando un usuario entra al sistema, como se mantiene la sesion, como se cierra, que controles de seguridad existen y que falta validar antes de declarar production-ready.

La idea central cambio respecto a la documentacion anterior:

El navegador ya no debe manejar access token ni refresh token como datos legibles por JavaScript. Esos tokens viajan en cookies HttpOnly. El frontend hidrata el usuario llamando al backend, no decodificando un JWT guardado en storage.

El modelo de autenticacion por cookies ya esta implementado y probado localmente con Playwright. En desarrollo local normal se usa por default mediante los scripts npm run dev del backend y npm run serve del frontend. En production-like y produccion sigue detras de configuracion explicita y validaciones de rollout. Eso significa:

  • El destino de seguridad aprobado es autenticacion basada en cookies.
  • Local normal usa cookies D2 para evitar volver al contrato legacy por accidente.
  • Legacy queda solo como opt-in local con npm run dev:legacy y npm run serve:legacy.
  • El sistema no esta marcado como production-ready.
  • El plan de rollout/rollback ya esta documentado.
  • La siguiente validacion real debe hacerse en un ambiente production-like con HTTPS y dominio real.

Si solo necesitas levantar el sistema local y confirmar que no estas usando legacy, usa primero Local D2 cookies para junior y luego Troubleshooting local D2.

Regla rapida para desarrollo local:

Backend: npm run dev
Frontend: npm run serve
Network: no debe existir Authorization
Cookies: medsync_at_dev, medsync_rt_dev, medsync_csrf_dev

Si ves Authorization en una peticion normal del frontend, lo mas probable es que estes usando legacy, una terminal vieja o residuos del navegador. No lo tomes como comportamiento normal de D2.

En local se valido que:

  • Las cookies auth locales medsync_at_dev y medsync_rt_dev son HttpOnly.
  • document.cookie no expone access/refresh.
  • localStorage y sessionStorage no contienen tokens.
  • Network del navegador no envia Authorization.
  • Refresh funciona con cookie y body {}.
  • Logout limpia cookies.
  • Super Admin close invalida la sesion afectada.
  • Tenant restricted y usuario inactivo quedan bloqueados en login sin cookies.

Lo que falta para production-ready:

  • Repetir browser QA en HTTPS/domain real.
  • Confirmar cookies Secure=true.
  • Confirmar cookies __Host-* si la topologia lo permite.
  • Validar reset password invalidation end-to-end con SMTP sandbox o humano autorizado.
  • Validar backup/restore, observabilidad y rollback drill.
Antes / legacyAhora / cookies HttpOnly
Frontend recibia token y a veces refresh_token en JSON.Backend setea access/refresh en cookies HttpOnly.
Axios enviaba Authorization.Axios usa withCredentials y no envia Authorization para el navegador.
Store/router decodificaban JWT desde storage.Frontend llama /api/auth/session para hidratar usuario, rol, permisos y tenant.
Refresh token podia vivir en storage cliente.Refresh token vive en cookie HttpOnly y DB conserva hash/rotacion/reuse detection.
CSRF no era central porque auth iba por header.CSRF es obligatorio porque el navegador adjunta cookies automaticamente.
Logout limpiaba storage y avisaba al backend.Logout revoca servidor y expira cookies; frontend limpia runtime y residuos legacy.
  1. Usuario abre /auth/login.
  2. Frontend pide GET /api/auth/csrf.
  3. Backend entrega token CSRF y setea cookie CSRF no HttpOnly.
  4. Usuario escribe telefono y password.
  5. Frontend llama POST /api/auth/login con:
    • credenciales;
    • X-CSRF-Token;
    • withCredentials=true.
  6. Backend valida payload, rate limit, usuario, password, tenant, CSRF y Origin/Referer.
  7. Si el usuario puede entrar, backend crea sesion server-side.
  8. Backend crea access token corto y refresh token v2.
  9. Backend setea cookies:
    • access cookie HttpOnly;
    • refresh cookie HttpOnly;
    • CSRF cookie visible para JS.
  10. Backend responde sin access token y sin refresh token en JSON.
  11. Frontend llama GET /api/auth/session.
  12. Backend devuelve usuario, rol, permisos, business y tenantOperational sin tokens.
  13. Store/Vuex guarda ese contexto solo en runtime.
  14. Router redirige segun rol, permisos y estado operacional.
flowchart TD
A["Usuario abre /auth/login"] --> B["GET /api/auth/csrf"]
B --> C["Cookie CSRF + token para header"]
C --> D["POST /api/auth/login con X-CSRF-Token"]
D --> E["Backend valida credenciales, CSRF, Origin y tenant"]
E --> F{"Puede entrar?"}
F -- "No" --> G["Error generico; sin sesion ni cookies auth"]
F -- "Si" --> H["Crear session + access + refresh v2"]
H --> I["Set-Cookie HttpOnly access/refresh"]
I --> J["GET /api/auth/session"]
J --> K["Frontend hidrata runtime Vuex"]
K --> L["Ruta protegida sin Authorization header"]

En production-like, la configuracion objetivo es:

CookieUsoHttpOnlySecureSameSitePathDomain
__Host-medsync_atAccess token cortoSiSiLax/Ninguno
__Host-medsync_rtRefresh token v2 rawSiSiLax/Ninguno
__Host-medsync_csrfCSRF double-submitNoSiLax/Ninguno

Reglas importantes:

  • Access y refresh deben ser HttpOnly; JavaScript no debe leerlos.
  • En production-like siempre deben usar Secure=true.
  • Si se usa prefijo __Host-, no puede existir Domain.
  • Path=/ es obligatorio para __Host-* y tambien ayuda a limpiar cookies correctamente.
  • La cookie CSRF no es HttpOnly porque el frontend necesita copiar el token al header.
  • Local HTTP puede usar nombres sin __Host- y Secure=false, pero eso no cuenta como production-like.

Con cookies, el navegador envia credenciales automaticamente. Por eso este modelo necesita CSRF.

El modelo usado es signed double-submit:

  • Backend emite una cookie CSRF legible por JavaScript.
  • Frontend manda el mismo valor en X-CSRF-Token.
  • Backend valida cookie, header, firma, expiracion y Origin/Referer.

Requests que deben llevar CSRF:

  • POST /api/auth/login.
  • POST /api/auth/refreshToken.
  • PUT /api/auth/close o logout equivalente.
  • POST, PUT, PATCH, DELETE protegidos.
  • Forgot password public endpoints si el flujo de cookies los protege con bootstrap pre-auth.

Errores esperados:

CasoResultado esperado
Falta header CSRF403.
Falta cookie CSRF403.
Cookie/header no coinciden403.
Firma invalida403.
Origin no permitido403.
Token validoLa request llega al controller o siguiente middleware.

La autenticacion por cookies necesita CORS con credenciales porque frontend y API viven en dominios distintos:

Frontend: https://app.<dominio>
API: https://api.<dominio>

Reglas:

  • CORS_CREDENTIALS=true.
  • CORS_ALLOWED_ORIGINS=https://app.<dominio>.
  • Nunca usar * con credentials.
  • Preflight debe permitir X-CSRF-Token.
  • Origenes no permitidos deben quedar fuera, sin credentials.
  • Vary: Origin debe estar presente cuando se refleja el origin.

El frontend no debe decodificar JWT para saber quien es el usuario.

La fuente de verdad de la sesion en este modelo es:

GET /api/auth/session

Ese endpoint devuelve:

  • authenticated.
  • user minimo.
  • role.
  • permissions.
  • business.
  • tenantOperational.
  • estado de sesion si aplica.

Nunca debe devolver:

  • access token;
  • refresh token;
  • token hash;
  • fingerprint completo;
  • password;
  • OTP;
  • secretos CSRF internos.

Esto permite que reload, nueva pestana y arranque de la app funcionen sin storage persistente de tokens.

DatoDonde debe vivirPuede persistir largo plazo?
Access tokenCookie HttpOnlyNo.
Refresh tokenCookie HttpOnly + hash/metadata server-sideNo en JS.
CSRF tokenCookie no HttpOnly y runtime frontendSolo como token no autenticante.
Usuario/rol/permisosVuex/runtime tras /api/auth/sessionNo.
Business/tenantOperationalVuex/runtime tras /api/auth/sessionNo.
Remembered identifiermedsync.auth.rememberedIdentifier.v1Si, es la unica excepcion auth-related.
Password/OTPSolo memoria/formulario temporalNo.

La clave permitida medsync.auth.rememberedIdentifier.v1 guarda solo un identificador de conveniencia, por ejemplo telefono recordado. No autentica, no autoriza y no debe contener tokens.

EndpointMetodoUso en autenticacion por cookies
/api/auth/csrfGETEntrega token CSRF y cookie CSRF.
/api/auth/loginPOSTValida credenciales y setea cookies auth; respuesta tokenless.
/api/auth/sessionGETHidrata usuario, rol, permisos y tenant sin tokens.
/api/auth/refreshTokenPOSTLee refresh cookie, rota refresh token v2 y renueva cookies.
/api/auth/closePUTCierra sesion actual, revoca servidor y expira cookies.
/api/auth/forze-closePUTCierre forzado legacy/protegido; debe mantenerse seguro.
/api/auth/sendOtpPOSTInicia recuperacion de password.
/api/auth/validateOtpPOSTValida codigo OTP/challenge.
/api/auth/resetPasswordPUTCambia password e invalida sesiones/refresh tokens.

El refresh token permite renovar el access token sin pedir password otra vez.

En este modelo:

  1. Frontend llama POST /api/auth/refreshToken.
  2. El body del navegador debe ser {}.
  3. El refresh token real se lee desde cookie HttpOnly.
  4. Backend valida el refresh token v2.
  5. Backend rota el refresh token.
  6. Backend setea nuevas cookies.
  7. La respuesta no incluye access token ni refresh token.
  8. Frontend llama o conserva /api/auth/session para seguir hidratado.

Controles esperados:

  • hash/firma server-side;
  • binding a id_session;
  • rotation;
  • family;
  • reuse detection;
  • revocacion por logout, Super Admin close y reset password.

Si un refresh viejo se reutiliza, debe fallar y disparar el alcance de revocacion configurado.

Logout debe limpiar los dos lados: servidor y navegador.

Backend:

  • revoca la sesion;
  • revoca refresh tokens asociados;
  • expira access cookie;
  • expira refresh cookie;
  • expira o rota CSRF cookie segun contrato.

Frontend:

  • limpia Vuex/runtime;
  • limpia CSRF runtime;
  • limpia residuos legacy de storage;
  • redirige a /auth/login;
  • debe hacer limpieza local aunque la llamada de logout falle.

Un 401 puede ocurrir porque:

  • access cookie expiro;
  • access cookie falta;
  • sesion fue cerrada;
  • Super Admin cerro la sesion;
  • reset password invalido sesiones;
  • refresh fue revocado o reutilizado;
  • backend ya no reconoce id_session.

Cuando ocurre, el frontend debe:

  • limpiar runtime;
  • limpiar storage auth legacy;
  • olvidar CSRF runtime;
  • evitar loops con /api/auth/session y /api/auth/csrf;
  • mandar al login cuando aplique.

Super Admin puede cerrar sesiones de otros usuarios. Ese flujo es importante para respuesta a incidentes.

Comportamiento esperado:

  1. Super Admin ejecuta cierre individual o masivo.
  2. Backend revoca la sesion afectada.
  3. Backend revoca refresh tokens relacionados.
  4. El usuario afectado recibe 401 en el siguiente request.
  5. Frontend limpia runtime/storage y vuelve al login.

La prueba local con Playwright valido el caso individual: el usuario QA afectado quedo en 401 despues del cierre.

Reset password no debe cambiar solo la contrasena. Tambien debe invalidar sesiones y refresh tokens previos.

Validacion obligatoria antes de production-ready:

  1. Usuario QA inicia sesion.
  2. Se confirma que access/refresh estan activos.
  3. Se ejecuta forgot/reset password con SMTP sandbox o humano autorizado.
  4. El access viejo falla.
  5. El refresh viejo falla.
  6. Password anterior falla.
  7. Password nueva funciona.
  8. Frontend limpia estado tras 401.

Reglas:

  • No imprimir OTP.
  • No imprimir passwords.
  • No imprimir tokens.
  • No inventar credenciales.
  • Si no hay SMTP sandbox, documentarlo como pendiente humano.

La decision CTO/Product para MVP/productivo inicial esta cerrada:

  • Tenant restricted/suspended/inactive en login: hard-block.
  • Usuario inactivo/bloqueado en login: hard-block.
  • No se emite sesion.
  • No se emite access cookie.
  • No se emite refresh cookie.
  • No se expone PHI.
  • Se mantiene error generico.

Esto es intencional. No es un bug.

La ruta /tenant-restricted se conserva para otro caso: cuando ya existia una sesion y el tenant cambia de estado despues. Por ejemplo:

  • usuario estaba logueado;
  • tenant pasa a suspendido;
  • /api/auth/session detecta tenantOperational.allowed=false;
  • frontend redirige a /tenant-restricted.

No se implementa ahora login limitado para billing/self-service. Ese tema queda como backlog futuro:

Limited restricted tenant session for billing/self-service UX

La autorizacion sigue teniendo tres capas:

CapaDonde se validaPara que sirve
Rolbackend/frontendDistinguir super_admin, admin tenant, doctor, assistant, recepcion, etc.
Permisossession DTO + guards/UI + endpoints protegidosControlar funciones especificas.
Tenant operationalbackend y session hydrationEvitar acceso a tenants no operativos.

Frontend puede ocultar menus o redirigir, pero la seguridad real debe estar en backend. Cualquier endpoint sensible debe validar rol, permiso o tenant segun corresponda.

AreaArchivoResponsabilidad
Login UIhemia-assistance-front-legacy/src/views/Auth/Login.vueFormulario, CSRF/bootstrap, login y ruta post-login.
Auth service frontendhemia-assistance-front-legacy/src/services/core/auth/DAAuthService.jsLlamadas login, session, refresh, logout, forgot/reset.
CSRF service frontendhemia-assistance-front-legacy/src/services/core/auth/DACsrfService.jsObtiene y adjunta X-CSRF-Token.
Session service frontendhemia-assistance-front-legacy/src/services/core/auth/DASessionService.jsBootstrap de /api/auth/session.
Axios helperhemia-assistance-front-legacy/src/services/config/axios.helper.jswithCredentials, CSRF y ausencia de Authorization en navegador.
Config frontend authhemia-assistance-front-legacy/src/services/config/authTransport.config.jsFlags de transporte por cookie.
Store authhemia-assistance-front-legacy/src/store/modules/auth/*Estado runtime de sesion.
Routerhemia-assistance-front-legacy/src/router/index.jsSession hydration, guards y redireccion.
Cleanup frontendhemia-assistance-front-legacy/src/utils/d2AuthCleanup.jsLimpieza runtime/storage legacy.
Cookie config backendhemia-assistance-back-legacy/src/config/authCookie.config.jsNombres y flags de cookies auth.
Security confighemia-assistance-back-legacy/src/config/security.jsStartup validation, CSRF/CORS/security env.
Auth routeshemia-assistance-back-legacy/src/routes/mysql/auth.jsEndpoints auth y middleware.
Auth controllerhemia-assistance-back-legacy/src/controller/mysql/auth.controller.jsLogin, session, refresh, logout, forgot/reset.
Auth DAOhemia-assistance-back-legacy/src/Dao/mysql/auth.dao.jsValidacion usuario/password, session, refresh, tenant.
Auth middlewarehemia-assistance-back-legacy/src/auth/authenticate.jsLee access cookie y conserva soporte legacy.
CSRF middlewarehemia-assistance-back-legacy/src/middleware/csrf.middleware.jsValida signed double-submit.
CORS middlewarehemia-assistance-back-legacy/src/middleware/corsCredentialed.middleware.jsAllowlist credentialed.

Backend production-like:

NODE_ENV=production
AUTH_COOKIE_MODE=d2
BROWSER_AUTH_TRANSPORT=cookie
REQUIRE_ACTIVE_SESSION=true
AUTH_COOKIE_ACCESS_NAME=__Host-medsync_at
AUTH_COOKIE_REFRESH_NAME=__Host-medsync_rt
AUTH_CSRF_COOKIE_NAME=__Host-medsync_csrf
AUTH_COOKIE_SECURE=true
AUTH_COOKIE_SAMESITE=lax
AUTH_COOKIE_DOMAIN=
AUTH_COOKIE_PATH=/
AUTH_COOKIE_MAX_AGE_MODE=session
CSRF_ENABLED=true
CORS_CREDENTIALS=true
CORS_ALLOWED_ORIGINS=https://app.<dominio>
CSRF_ALLOWED_ORIGINS=https://app.<dominio>

Frontend production-like:

VUE_APP_AUTH_COOKIE_MODE=d2
VUE_APP_BROWSER_AUTH_TRANSPORT=cookie
VUE_APP_CSRF_ENABLED=true
VUE_APP_CORE_URL_API=https://api.<dominio>/

Nota: el valor literal d2 en AUTH_COOKIE_MODE y VUE_APP_AUTH_COOKIE_MODE es el nombre tecnico que usa el codigo para activar autenticacion por cookies. No representa una etapa de trabajo interna ni una instruccion de planificacion.

Local HTTP default:

# Backend
AUTH_COOKIE_MODE=d2
BROWSER_AUTH_TRANSPORT=cookie
AUTH_COOKIE_ACCESS_NAME=medsync_at_dev
AUTH_COOKIE_REFRESH_NAME=medsync_rt_dev
AUTH_CSRF_COOKIE_NAME=medsync_csrf_dev
AUTH_COOKIE_SECURE=false
AUTH_COOKIE_SAMESITE=lax
AUTH_COOKIE_DOMAIN=
AUTH_COOKIE_PATH=/
AUTH_COOKIE_MAX_AGE_MODE=session
CSRF_ENABLED=true
CORS_CREDENTIALS=true
CORS_ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080,http://localhost:8081,http://127.0.0.1:8081,http://localhost:8082,http://127.0.0.1:8082,http://localhost:8098,http://127.0.0.1:8098
CSRF_ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080,http://localhost:8081,http://127.0.0.1:8081,http://localhost:8082,http://127.0.0.1:8082,http://localhost:8098,http://127.0.0.1:8098
# Frontend
VUE_APP_AUTH_COOKIE_MODE=d2
VUE_APP_BROWSER_AUTH_TRANSPORT=cookie
VUE_APP_CSRF_ENABLED=true
VUE_APP_CORE_URL_API=http://127.0.0.1:3009/

Secretos reales:

  • deben venir de secret manager o mecanismo infra equivalente;
  • nunca deben commitearse;
  • nunca deben copiarse a docs, tickets o chats.

En la validacion production-like, revisar:

  • Login tokenless.
  • Access cookie HttpOnly=true.
  • Refresh cookie HttpOnly=true.
  • Cookies auth Secure=true.
  • SameSite=Lax.
  • Path=/.
  • __Host-* sin Domain si aplica.
  • document.cookie sin access/refresh.
  • localStorage sin tokens.
  • sessionStorage sin tokens.
  • Network sin Authorization.
  • Mutaciones con X-CSRF-Token.
  • Refresh con body {}.
  • Logout limpia cookies.
  • Reload y nueva pestana rehidratan /api/auth/session.
  • Super Admin close produce 401.
  • Tenant restricted hard-block sin cookies.
  • Usuario inactivo hard-block sin cookies.
  • Flujo clinico minimo no usa tokens manuales.
SintomaRevisar primero
Login devuelve 403CSRF header/cookie, Origin, CSRF_ALLOWED_ORIGINS.
Login devuelve 401/404 genericoCredenciales, usuario activo, tenant hard-block, rate limit.
Login responde pero no hay sesionCookies bloqueadas, CORS credentials, withCredentials, domain/path.
/api/auth/session no autenticaAccess cookie ausente/expirada, CORS, cookie path/domain, sesion revocada.
Refresh fallaRefresh cookie, CSRF, body {}, reuse detection, sesion activa.
Logout no limpiaAtributos de cookie no coinciden entre set y clear.
Aparece Authorization en NetworkFrontend no esta compilado con transporte por cookies o cayo a legacy.
Token aparece en storageBug bloqueante; detener rollout.
Tenant restricted entra al sistemaBug bloqueante; debe hard-block en login.
Reset password no cierra sesion viejaNo marcar production-ready; revisar invalidacion server-side.

Lo que ya no debe documentarse como estado actual

Section titled “Lo que ya no debe documentarse como estado actual”

Esta pagina reemplaza el modelo anterior. Ya no debe tratarse como destino final:

  • Authorization como auth de navegador.
  • access token en VueSession.
  • refresh token legible por JavaScript.
  • “Mantener sesion iniciada”.
  • router basado en JWT decodificado desde storage.
  • permisos/tenant/business persistidos a largo plazo.

Puede existir compatibilidad legacy para desarrollo, pruebas o clientes no navegador, pero no es el contrato objetivo del navegador.

El objetivo es que un XSS o un equipo clinico compartido no puedan robar tokens desde JavaScript o storage. Para lograrlo, el navegador autentica con cookies HttpOnly, el frontend usa CSRF para mutaciones y la identidad visible se hidrata desde /api/auth/session.

El sistema ya tiene base fuerte: sesiones server-side, refresh token v2, logout, Super Admin close, tenant hard-block y limpieza frontend. Lo que falta para production-ready no es escribir mas explicacion: falta validarlo en un ambiente production-like real con HTTPS, cookies Secure=true, dominios finales, SMTP/reset password, backups, observabilidad y rollback probado.