# 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. ```mermaid 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 - [x] El niño usa la app sin credenciales tras el primer login del adulto. - [x] `/api/parents/**` inaccesible sin PIN (test verde). - [x] 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`