Files
recordalexia/docs/adr/adr-001-autenticacion-propia-sesion-familia.md

4.5 KiB
Raw Blame History

ADR-001: Autenticación propia por sesión de familia (sin Keycloak)

Metadata Valor
Estado Aceptado (implementado)
Fecha 2026-07-12
Autor(es) Jaume Garriga Maestre
Supersede —

Contexto

recordaLexia es una app doméstica/homelab para niños con TDAH que corre en una tablet en modo kiosko. Fuera del stack corporativo ECA por diseño: no hay Gravitee, ni Kubernetes, ni infraestructura gestionada donde apoyar un IdP.

Requisitos que condicionan la autenticación:

  • Fricción cero para el niño: abre la tablet y ve su día. Un niño de 7 años con TDAH no puede depender de contraseñas ni de flujos de login.
  • Multi-tenant real: varias familias en la misma instancia, con aislamiento estricto de datos (un niño de la familia A no existe para la familia B).
  • Zona de configuración protegida: el panel de padres debe quedar fuera del alcance del niño sin romper el kiosko.
  • Operación doméstica: un contenedor más en docker-compose es coste asumible; operar un Keycloak (memoria, actualizaciones, backups del realm) no lo es.

Opciones Consideradas

Opción A: Keycloak OAuth2 (estándar corporativo)

Pros Contras
Estándar Asepeyo, flujos probados Un servicio pesado más que operar en un homelab
SSO, federación, MFA disponibles Fricción de login incompatible con el kiosko infantil
Separación IdP/aplicación Sobredimensionado: no hay federación ni múltiples clientes

Opción B: Auth propia por sesión de familia + PIN (elegida)

Pros Contras
Cero fricción para el niño (la sesión vive en el dispositivo) Implementación propia: superficie de error en código nuestro
Dos niveles con un mecanismo (rol FAMILY / rol PARENT vía PIN) Sin SSO ni MFA (irrelevantes en el contexto doméstico)
Nada extra que operar; BCrypt + tabla de sesiones Rotación/expiración de sesión hay que diseñarla a mano

Decisión

Opción B. El adulto se registra/loguea una vez; el dispositivo conserva una sesión de familia (FamilySession, handle opaco + expiración) que da el rol FAMILY a toda la API del kiosko. El panel de padres (/api/parents/**) exige el rol PARENT, que se obtiene desbloqueando con PIN (POST /api/parents/unlock) y caduca solo. Contraseña y PIN con hash BCrypt. Todo scopeado a FamilyContext.currentFamilyId().

La clase AuthService queda encapsulada precisamente para poder sustituirla por un IdP externo sin tocar el resto del código, si el contexto cambia.

sequenceDiagram
    participant A as Adulto
    participant K as Tablet kiosko
    participant API as Backend
    A->>API: register/login (una vez)
    API-->>K: sesión de familia (rol FAMILY)
    Note over K: el niño usa la app sin loguearse
    A->>API: PIN → unlock (rol PARENT, temporal)
    API-->>A: panel de padres

Trade-offs

Optimizamos para:

  • Usabilidad del niño: el requisito nº 1 del producto; cualquier fricción de auth rompe el caso de uso.
  • Operabilidad doméstica: menos piezas que mantener en un homelab.

Sacrificamos:

  • Robustez de un IdP maduro: aceptable porque la superficie expuesta es la red local doméstica y los datos, aunque personales, no salen de casa.
  • Estándares corporativos (Keycloak): desviación consciente y documentada; el proyecto está explícitamente fuera del ECA.

Asunciones que podrían invalidar esta decisión:

  • Si la app se expusiera a Internet o se ofreciera como SaaS multi-hogar, reconsiderar: haría falta IdP real, MFA y hardening de sesiones.
  • Si Asepeyo integrara este producto en su cartera, aplicaría el stack ECA (Keycloak + Gravitee) y este ADR quedaría supersedido.

Consecuencias

  • (+) Kiosko sin fricción; multi-tenant con una sola tabla extra.
  • (+) CORS/CSRF simplificados (API stateless con cabecera de sesión propia).
  • (−) El equipo asume el mantenimiento del código de auth → mitigado con tests de integración (AuthIT: registro, login fallido, gate por PIN, aislamiento entre familias).

Criterios de Éxito

  • El niño usa la app sin credenciales tras el primer login del adulto.
  • /api/parents/** inaccesible sin PIN (test verde).
  • Una familia no puede ver datos de otra (test verde, 404 para no filtrar existencia).

Referencias

  • backend/src/main/java/es/asepeyo/recordalexia/security/ (SecurityConfig, AuthService, SessionAuthFilter, FamilyContext)
  • backend/src/test/java/es/asepeyo/recordalexia/web/AuthIT.java