ADR-0009: La identidad es externa y el LIS es un resource server
Estado: Aceptado · Fecha: 2026-09-25
Contexto
El pom.xml traía spring-security-webauthn y la intención declarada era ofrecer passkeys, OTT,
MFA y login con Google. Todos esos flujos son interactivos de navegador y con estado, y eso
choca con dos decisiones ya tomadas:
| Flujo | Qué necesita | Contra qué choca |
|---|---|---|
| Passkeys / WebAuthn | PublicKeyCredentialCreationOptions en la HttpSession por defecto, y POST con CSRF |
STATELESS y CSRF desactivado (ADR-0005) |
| OTT / magic link | Challenge persistido, POST con CSRF, página de submit | ADR-0005 y D-03 |
| OAuth2 Login | AuthorizationRequest guardado entre redirecciones del navegador |
ADR-0005 |
| OIDC Back-Channel Logout | Exige que la cookie de sesión se llame JSESSIONID |
No hay sesión |
| MFA | Redirige al usuario a la página del factor que falta | No hay páginas (D-03) |
La pregunta se resolvió preguntando a la documentación en lugar de eligiendo por gusto. El mecanismo de autenticación no aparece ni una vez en el marco conceptual ni en procesos y casos de uso: ni contraseña, ni MFA, ni passkey, ni SSO, ni federación. Lo que sí exigen es otra cosa:
El modelo distingue dos identidades y los procesos deben respetar esa separación:
cuenta_usuarioregistra quién ejecuta la operación yprofesional_laboratorioregistra quién aprueba o firma. Una cuenta puede enlazarse con su profesional, pero no lo sustituye. Todas las acciones clínicas y administrativas guardan actor, fecha y contexto.
El requisito es atribución y auditoría, no fortaleza de credencial. P15 se define como «acceso
controlado, evidencia y alertas» sobre cuenta_usuario, cuenta_rol, evento_auditoria y
alerta.
Conviene nombrar la confusión de categorías que esto evita: la firma del informe no se resuelve
con MFA. Se resuelve con informe_resultados.firmante apuntando a un profesional_laboratorio
más el hash_sha256 del artefacto, que ya están en el esquema. Es un registro de dominio, no un
mecanismo de sesión. Autenticar más fuerte no firma un informe.
Decisión
La identidad la emite un proveedor externo. El LIS valida tokens y no hospeda login. Cada
petición a /api/** se autentica por sí misma con su token; ADR-0005 se conserva intacto y deja de
tener una cláusula pendiente.
Passkeys, OTT, MFA y login con Google no se cancelan: se mudan. Pasan a ser configuración del proveedor de identidad, que los trae probados y mantenidos, en lugar de código de este servicio.
Salen del pom.xml:
spring-security-webauthn, porque este servicio ya no registra ni verifica credenciales.thymeleaf-extras-springsecurity6, porque solo sirve para etiquetassec:en páginas HTML.
Thymeleaf se queda, y su propósito queda escrito para que no vuelva a confundirse: rellenar
plantillas de mensaje —cuerpos de correo, avisos— no servir páginas. Por eso el directorio se llama
plantillas y tiene su propio README.
Evidencia en el modelo de datos
Cuatro hechos, en orden de contundencia:
- Ninguna de las 90 tablas almacena credenciales, passkeys, tokens ni sesiones. Spring Security
WebAuthn exigiría añadir por migración las tablas de
JdbcPublicKeyCredentialUserEntityRepositoryyJdbcUserCredentialRepository. Un modelo que cubre microbiología, inventario y cartera no se olvidó de las credenciales: decidió no tenerlas. - La unicidad de la identidad externa es global; la del nombre es por organización.
unique (proveedor_identidad, identificador_externo)frente aunique (organizacion_id, nombre_acceso). Lo primero es exactamente la semántica de issuer más subject de un token OIDC. Si el LIS emitiera la identidad, esas dos columnas serían redundantes y no necesitarían unicidad global. - El comentario de la tabla: «referencia a una identidad externa; no almacena contraseñas ni
secretos reversibles». Y los datos de demostración ya usan
proveedor_identidad = 'oidc-demo'. - D-03 lo refuerza. Dice que la UI está fuera del plan pero «la UI llegará», con las pantallas ya diseñadas. Si mañana llega una interfaz, es un cliente separado y el LIS sigue validando tokens. Las páginas por defecto de Spring Security serían HTML servido por este servicio, que es justo lo que D-03 excluye.
Consecuencias
- ADR-0005 deja de tener una cláusula condicional: no habrá autenticación por cookie en esta API, así que CSRF queda desactivado de forma permanente y razonada, no provisional.
- El servicio no custodia credenciales. Lo que no se almacena no se filtra.
- Passkeys y MFA llegan antes y mejor hechos que si se implementaran aquí.
- Una dependencia menos en el classpath, y un esquema que no necesita migraciones para tablas de
infraestructura de autenticación.
− Dependencia operativa de un proveedor de identidad: hay que desplegarlo, configurarlo y mantenerlo.
− Desarrollar contra un IdP es más incómodo que contra un usuario en memoria. Se mitiga conservando
el perfil
devcon Basic hasta M8.
Qué cambia en el código, y cuándo
| Cambio | Cuándo |
|---|---|
spring-security-webauthn y thymeleaf-extras-springsecurity6 fuera |
Ahora |
spring-boot-starter-oauth2-resource-server dentro, validación de JWT, SecurityFilterChain propio |
M8 |
ResolverContexto busca por (proveedor_identidad, identificador_externo) = (iss, sub) del token, no por nombre_acceso |
M8 |
Autorización por rol y por recurso sobre cuenta_rol |
M8 |
Basic con usuario del perfil dev como andamiaje |
Hasta M8 |
La resolución actual por nombre_acceso (S1.01a) es provisional y está anotada como tal: funciona
porque el usuario de desarrollo es un nombre, pero la clave natural de una identidad externa es la
pareja proveedor más identificador, que es su restricción única global.
Lo que esta decisión NO resuelve
No elige el proveedor concreto ni cómo se despliega. Esa elección depende de una variable de producto que sigue abierta: si el LIS se vende como caja única en el laboratorio del cliente, exigir un IdP al lado sube el coste de instalación, y entonces habría que reconsiderar un servidor de autorización dentro del mismo despliegue. Si la instalación la opera el proveedor del software, el IdP externo gana sin discusión. Se decide antes de M8, no antes de M1.
No define el contrato de errores del borde de seguridad. Eso es de ADR-0008, todavía sin escribir,
que heredará de aquí un supuesto firme: un 401 lleva WWW-Authenticate y cuerpo problem+json, sin
redirecciones, porque no hay página de login a la que volver.
No toca SecurityConfig todavía: sigue siendo Basic contra un usuario en memoria, que es suficiente
andamiaje para M1 y se sustituye en M8.
Alternativas descartadas
El LIS emite su identidad, con dos cadenas de filtros. Una con estado y CSRF para /auth/** y
otra stateless para /api/**. Conserva un solo despliegue (D-05) y spring-security-webauthn, pero
obliga a emitir tokens propios —la parte del sistema donde un error cuesta más caro y donde menos
valor propio se aporta— y a añadir al esquema tablas de credenciales que el modelo no contempló.
Queda como el camino a revisar solo si el despliegue en caja única lo exige.
Un servidor de autorización separado, en otro proceso. Resuelve lo mismo que el IdP externo pero hay que escribirlo y mantenerlo, y contradice D-05 (un despliegue, un equipo de una persona).