SSO entre aplicaciones del mismo tenant¶
Si varias aplicaciones cliente están integradas con el Verifier de un mismo tenant de EUDIStack —tuyas o de terceros, como un portal de gestión propio y el portal de un proveedor—, el Verifier puede mantener una sesión de autenticación a nivel de tenant: el usuario presenta su credencial una sola vez y las demás aplicaciones reutilizan esa sesión sin pedirle que vuelva a escanear el QR. Para ello, cada aplicación debe estar dada de alta en el catálogo de aplicaciones elegibles del tenant.
Esta guía parte de que tu aplicación ya está integrada con el Verifier como OIDC IdP (login con credencial). El SSO no cambia el contrato OIDC que ya integraste: con el login estándar, el Verifier reutiliza la sesión si existe. Añade, de forma opcional, una comprobación silenciosa de la sesión (prompt=none) y el cierre de sesión coordinado entre aplicaciones.
En resumen
- Cumple los requisitos: SSO activado en el tenant y tu
client_iden el catálogo de aplicaciones elegibles. - Lanza el login OIDC estándar, sin
prompt: si hay sesión SSO, el Verifier devuelve elcodesin QR; si no, muestra el QR como siempre. Usaprompt=nonesolo si necesitas comprobar la sesión sin mostrar el login. - Cierra sesión a través del Verifier y, si puedes, escucha los avisos de logout de las demás aplicaciones.
Desde la versión 3.4.0 del Verifier
Hasta la versión 3.3, el Verifier solo reutilizaba la sesión cuando la petición llevaba prompt=none, y por eso cada aplicación tenía que pedir primero el login en silencio. Desde la 3.4.0 basta con el login estándar. Si tu aplicación ya hace el intento con prompt=none, sigue funcionando igual.
Requisitos¶
| Requisito | Quién lo gestiona | Detalle |
|---|---|---|
| SSO activado en el tenant | Equipo de EUDIStack | Se habilita ssoEnabled y se configura el dominio raíz (rootDomain) del tenant, al que se asocia la cookie de sesión y que debe abarcar el host en el que se sirve su Verifier. |
| Aplicación registrada como cliente OIDC del tenant | Equipo de EUDIStack | Es el mismo alta que necesitas para el login con credencial. |
client_id en el catálogo de aplicaciones elegibles | Equipo de EUDIStack | Se da de alta a petición tuya. Sin este paso, tu aplicación nunca reutiliza la sesión: el usuario ve siempre el QR (con prompt=none, recibes interaction_required). |
Para solicitar la activación o el alta de una aplicación, contacta con el equipo de EUDIStack indicando el tenant y los client_id implicados.
Dar de alta una aplicación es confiarle la sesión del usuario
Mientras la sesión SSO esté vigente, cualquier client_id del catálogo obtiene un id_token del usuario sin que este vuelva a presentar su credencial ni vea ninguna pantalla. Esto aplica igual a las aplicaciones de terceros: da de alta solo aquellas en las que confíes como en las tuyas, y retíralas del catálogo en cuanto dejen de necesitar el acceso.
Cómo funciona¶
Cada tenant con SSO activado mantiene, además de las sesiones OIDC de cada aplicación, una sesión de autenticación propia del tenant ("OP-level session" en la terminología de OIDC Core). Se guarda en una cookie opaca, asociada al dominio raíz del tenant, que el navegador del usuario presenta al Verifier en cada petición de autorización, venga de la aplicación que venga.
sequenceDiagram
autonumber
participant App1 as Aplicación A
participant App2 as Aplicación B
participant Browser as Browser del usuario
participant Verifier as EUDIStack Verifier
participant Wallet as EUDI Wallet
Note over App1,Wallet: 1. Primer login (idéntico al flujo estándar)
App1->>Browser: redirige a /verifier/oidc/authorize
Browser->>Verifier: Authorization Request
Verifier-->>Browser: página de login + QR (+ cookie de vínculo del login)
Browser->>Wallet: el usuario escanea el QR y presenta su credencial
Wallet->>Verifier: respuesta de presentación válida
Verifier-->>Browser: aviso a la página de login: login completado
Browser->>Verifier: cierre del login (con la cookie de vínculo)
Verifier-->>Browser: redirect a App A + code + Set-Cookie __Secure-sso-<tenant>
App1->>Verifier: canje del code
Verifier-->>App1: id_token (con claim sid) + access_token
Note over App2,Verifier: 2. Segunda aplicación, mismo usuario, misma sesión de navegador
App2->>Browser: redirige a /verifier/oidc/authorize (login estándar, sin prompt)
Browser->>Verifier: GET /authorize (Cookie __Secure-sso-<tenant>)
Verifier->>Verifier: sesión válida + App B en el catálogo SSO
Verifier-->>Browser: redirect a App B + code (sin UI ni QR)
App2->>Verifier: canje del code
Verifier-->>App2: id_token (con claim sid) + access_token La sesión SSO se establece automáticamente tras cualquier login con credencial en un tenant con SSO activado, sea cual sea la aplicación. Qué aplicación es la "primera" depende de por dónde entre el usuario; como la reutilización ocurre en el propio login estándar, todas las aplicaciones del catálogo la aprovechan sin hacer nada especial.
La cookie de sesión se emite en el navegador que inició el login, aunque el usuario presente su credencial desde la wallet de otro dispositivo (el móvil): tras la presentación, la página de login devuelve el navegador al Verifier para cerrar el login, y es en ese paso donde el Verifier crea la sesión y redirige a tu aplicación. Este paso es transparente para tu aplicación, que recibe el code en su redirect_uri como siempre.
Integrar el login¶
Lanza la petición de autorización estándar (authorization code + PKCE), sin prompt. El Verifier decide si hace falta el QR:
flowchart TD
A["Tu aplicación necesita<br>autenticar al usuario"] --> B["/authorize<br>(login estándar, sin prompt)"]
B --> C{"¿Sesión SSO vigente<br>y tu aplicación<br>en el catálogo?"}
C -->|sí| D["code en tu redirect_uri<br>(entra sin QR)"]
C -->|no| E["Página de login con QR"]
E --> D GET /verifier/oidc/authorize?
response_type=code&
client_id=mi-segunda-app&
scope=openid learcredential.employee&
state=Xq7mZ2pL9vT4rN8sK3wY&
redirect_uri=https://mi-segunda-app.com/callback&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Genera un state nuevo y aleatorio en cada petición
El Verifier identifica cada login en curso por su state. Si tu aplicación lanza un login con QR reutilizando un state que todavía está en curso (hasta 10 minutos), el Verifier lo rechaza con invalid_request: state already in use y muestra su página de error, salvo que sea un reintento desde el mismo navegador. Las librerías OIDC habituales ya generan un state distinto en cada petición; no uses valores fijos ni predecibles.
Comportamiento según prompt¶
prompt | Sesión SSO reutilizable | Sin sesión reutilizable |
|---|---|---|
| Ausente | code sin QR | Login con QR, como siempre |
none | code sin QR | error=login_required o error=interaction_required, sin mostrar ninguna pantalla (ver abajo) |
login | Login con QR: el usuario presenta su credencial de nuevo. La sesión SSO no se cierra | Login con QR |
none junto a otro valor (p.ej. none login) | error=invalid_request | error=invalid_request |
La sesión es reutilizable cuando el navegador presenta una sesión SSO vigente del tenant, tu client_id está en el catálogo y, si envías max_age, la sesión es más reciente que ese límite. Los valores de prompt que el Verifier no soporta (consent, select_account) se tratan como ausentes.
Si la petición lleva un request object (request o request_uri), el Verifier lee prompt y max_age solo de los claims del JWT firmado (RFC 9101 §6.3) e ignora los de la query.
Forzar una autenticación nueva¶
Antes de una operación sensible puedes exigir que el usuario presente su credencial aunque haya sesión SSO:
prompt=login: siempre muestra el QR.max_age=<segundos>: muestra el QR si el login con credencial es más antiguo que ese número de segundos. Debe ser un entero no negativo; un valor mal formado (negativo, decimal o no numérico) recibeerror=invalid_request.
En ambos casos, el id_token trae el auth_time del login con credencial, así que también puedes comprobar la antigüedad tú mismo.
Respuesta en tu redirect_uri¶
Toda respuesta del Verifier en tu redirect_uri, de éxito o de error, conserva la query que ya tenga el redirect_uri registrado y añade:
| Parámetro | Éxito | Error |
|---|---|---|
code | Sí | — |
error | — | login_required, interaction_required, invalid_request, access_denied |
error_description | — | Texto fijo en inglés (p.ej. max_age must be a non-negative integer). Es informativo: no lo muestres tal cual al usuario |
state | El de tu petición | El de tu petición |
iss | Sí | Sí |
iss (RFC 9207) es el issuer que publica el discovery del Verifier del tenant (/.well-known/openid-configuration), que anuncia authorization_response_iss_parameter_supported: true. Si tu aplicación trabaja con varios servidores de autorización, compáralo con ese issuer para detectar ataques de mezcla (mix-up). Si no, puedes ignorarlo.
En el login con QR, tu redirect_uri también puede recibir ?error=access_denied: el login no se cerró en el navegador que lo inició (por ejemplo, porque el navegador bloquea las cookies del Verifier o porque se intentó completar desde otro navegador). El Verifier no entrega ni el code ni la sesión SSO en ese caso; tu aplicación debe tratarlo como un login fallido y permitir reintentarlo.
Si la petición no supera la validación del cliente, del redirect_uri registrado o de la firma del request object, el Verifier muestra su propia página de error y nunca redirige a tu redirect_uri.
Opcional: comprobar la sesión con prompt=none¶
Con prompt=none el Verifier nunca muestra pantallas ni QR: resuelve la petición en el momento o devuelve un error de inmediato. Solo te hace falta si quieres saber, al arrancar tu aplicación, si el usuario tiene sesión sin llevarlo a la página de login cuando no la tiene (por ejemplo, para mostrar una portada pública con un botón "Entrar"). Si tu aplicación lleva al login nada más arrancar, no lo necesitas.
Añade prompt=none a la petición anterior y gestiona la respuesta:
| Resultado | Qué significa | Qué hace tu aplicación |
|---|---|---|
?code=... | Hay sesión SSO vigente y tu aplicación está en el catálogo. | Canjea el code como en el flujo normal. |
?error=login_required | No hay sesión SSO utilizable: nunca se estableció, caducó o es de otro tenant. | Trata al usuario como no autenticado. Cuando tenga que entrar, lanza el login sin prompt. |
?error=interaction_required | Hay sesión SSO vigente, pero tu client_id no está en el catálogo. | Igual que login_required, y revisa los requisitos. |
Un solo intento, con redirección de página completa
Lanza el intento con prompt=none una sola vez por carga de la aplicación (guarda una marca, por ejemplo en sessionStorage), para no entrar en un bucle de redirecciones si el Verifier responde con error. Y hazlo con una redirección normal del navegador: el Verifier no permite que sus páginas se carguen dentro de un iframe, así que si tu librería OIDC hace el login silencioso en un iframe oculto (silent renew), desactívalo.
Distingue los dos errores en tus logs
login_required e interaction_required significan cosas distintas (OIDC Core §3.1.2.6). El primero es esperable (usuario nuevo, sesión caducada). El segundo indica un problema de configuración del catálogo que conviene investigar si se repite.
Cerrar sesión (Single Logout)¶
Para entender el logout hay que distinguir dos sesiones, cada una controlada por alguien distinto:
| Sesión de tu aplicación | Sesión SSO del tenant | |
|---|---|---|
| Quién la controla | Tu aplicación | El Verifier |
| Para qué sirve | Saber si el usuario está dentro de tu aplicación | Saber si el usuario puede entrar en otras aplicaciones sin escanear el QR |
| Cuándo termina | Cuando tu aplicación la cierra o caduca, según tu configuración | Al hacer logout en cualquier aplicación, o al caducar (ver Duración de la sesión) |
El Verifier no puede cerrar la sesión de tu aplicación
Solo decide si el próximo login necesita el QR o no. Por eso el logout tiene dos partes: cerrar la sesión SSO a través del Verifier y, cuando el logout empieza en otra aplicación, que tu aplicación cierre la suya al recibir el aviso.
Qué tiene que hacer tu aplicación en cada caso:
| Situación | Qué hace tu aplicación | ¿Obligatorio? |
|---|---|---|
| El usuario cierra sesión en tu aplicación | Redirigirlo al endpoint de logout del Verifier (caso 1) | Sí |
| El usuario cierra sesión en otra aplicación | Recibir el aviso del Verifier y cerrar su sesión local (caso 2) | Recomendado |
Caso 1: el usuario cierra sesión en tu aplicación¶
- Borra tu sesión local.
-
Redirige al usuario al endpoint de logout del Verifier (OIDC RP-Initiated Logout 1.0):
-
El Verifier cierra la sesión SSO al instante, avisa a las demás aplicaciones que usaron la sesión (ver caso 2) y devuelve al usuario a tu
post_logout_redirect_uri, que debe estar registrado para tuclient_id.
Borrar solo tu sesión local no cierra el SSO
Si tu aplicación se salta el paso 2, la sesión SSO sigue vigente y el siguiente login volverá a dar acceso al usuario sin pedirle la credencial.
Caso 2: el usuario cierra sesión en otra aplicación¶
El Verifier avisa a tu aplicación mediante OIDC Back-Channel Logout 1.0: una llamada servidor a servidor a tu backchannel_logout_uri, sin pasar por el navegador del usuario.
sequenceDiagram
autonumber
participant AppA as Aplicación A
participant Verifier as EUDIStack Verifier
participant AppB as Tu aplicación
AppA->>Verifier: GET /oidc/logout (el usuario cierra sesión en A)
Verifier->>Verifier: cierra la sesión SSO
Verifier->>AppB: POST backchannel_logout_uri (logout_token)
AppB->>AppB: cierra la sesión local de ese sid
AppB-->>Verifier: 200 OK La petición que recibe tu endpoint:
POST /backchannel-logout
Host: mi-segunda-app.com
Content-Type: application/x-www-form-urlencoded
logout_token=eyJ...
Y el contenido del logout_token una vez decodificado:
{
"iss": "https://empresa.eudistack.net/verifier",
"aud": "mi-segunda-app",
"sid": "f3a1c9e2b6d84f0a",
"iat": 1715003600,
"exp": 1715003720,
"jti": "5b0c6e2a-8d1f-4a7e-9c3b-2f6d1e8a4b90",
"events": { "http://schemas.openid.net/event/backchannel-logout": {} }
}
Tu endpoint debe:
- Validar el token: firma con las claves del
jwks_uridel Verifier, cabeceratyp=logout+jwt,iss,aud(tuclient_id),exp(el token caduca a los 2 minutos) y queeventscontienehttp://schemas.openid.net/event/backchannel-logout. - Cerrar la sesión local asociada al
sid. Es el mismosidque recibiste en elid_token, así que guárdalo al iniciar sesión. - Responder
200 OKen menos de 5 segundos. Cualquier otra respuesta se trata como fallo y el Verifier reintenta la entrega.
La aplicación que inició el logout no recibe este aviso.
Limitación actual: la aplicación del login con QR no recibe el aviso
El Verifier avisa a las aplicaciones que entraron reutilizando la sesión, con o sin prompt=none. La aplicación en la que el usuario hizo el login con QR —la que creó la sesión SSO— no queda registrada como destinataria y no recibe el logout_token, salvo que después haya vuelto a entrar reutilizando la sesión. Hasta que se corrija, esa aplicación solo se entera del logout cuando caduca su sesión local (ver el aviso siguiente).
Si tu aplicación no escucha el aviso, el usuario sigue dentro
El backchannel_logout_uri es opcional (HTTPS obligatorio), pero sin él tu aplicación no se entera de los logouts hechos en otras. Si el usuario cierra sesión en otra aplicación del tenant:
- Sigue dentro de tu aplicación hasta que caduque tu sesión local o cierre sesión también en ella.
- Cuando tu sesión local caduque, el siguiente login le mostrará el QR (con
prompt=none, recibiráslogin_required): el logout acaba afectando a tu aplicación, pero tarde. - Los tokens ya emitidos no se revocan. Tu
access_tokensigue siendo válido hasta que expire, así que tampoco sirve para detectar el logout.
El riesgo es que otra persona que use el mismo equipo, después de que el usuario crea haber cerrado sesión, siga teniendo acceso a tu aplicación. Si tu aplicación maneja datos sensibles, publica el backchannel_logout_uri.
Referencia¶
Contenido del id_token¶
En un tenant con SSO activado, el id_token incluye el claim sid en cualquier login: tanto en el primero, con QR, como en los que reutilizan la sesión. Ejemplo con los claims relevantes para el SSO (se omiten el resto de claims del login estándar):
{
"iss": "https://empresa.eudistack.net/verifier",
"sub": "juan.garcia@empresa.com",
"aud": "mi-segunda-app",
"sid": "f3a1c9e2b6d84f0a",
"auth_time": 1715000000,
"acr": "0"
}
sid identifica la sesión SSO compartida: todas las aplicaciones que usan la sesión, incluida la del primer login, reciben el mismo valor, y es el que llega en el logout_token. No es un secreto (viaja en un JWT firmado), pero evita exponerlo en URLs o logs de acceso.
auth_time es el momento del login con credencial y no cambia en los logins que reutilizan la sesión. acr vale siempre "0": el Verifier no declara un nivel de garantía en el id_token.
Duración de la sesión¶
La sesión SSO caduca en cuanto se cumple cualquiera de estos dos límites. Los valores por defecto se pueden ajustar por tenant dentro del rango, a petición al equipo de EUDIStack:
| Límite | Por defecto | Rango configurable |
|---|---|---|
| Tiempo de vida absoluto | 8 horas desde el login | 1 h – 24 h |
| Inactividad | 30 minutos sin uso | 5 min – 60 min |
Tras la caducidad, el login vuelve a mostrar el QR (y prompt=none recibe login_required): tu aplicación debe estar preparada para ello en cualquier momento, no solo en el primer acceso del día.
Seguridad¶
- Cookie de sesión. Se llama
__Secure-sso-<tenant>y se emite conSecure,HttpOnly,SameSite=LaxyDomain=<dominio raíz del tenant>. Es un identificador opaco de 256 bits, no un JWT: no contiene datos del usuario ni de la credencial. Solo la gestiona el Verifier; tu aplicación nunca la lee ni la modifica. - Cookie de vínculo del login. Al mostrar el QR, el Verifier emite además
__Host-sso-tx(Secure,HttpOnly,SameSite=Lax, sinDomain: solo para el host del Verifier; vida de 10 minutos). Liga el login al navegador que lo inició: el Verifier solo crea la sesión SSO y entrega elcodea ese navegador, de modo que quien observe el login desde fuera no puede apropiarse de él. Tampoco la gestiona tu aplicación. - Aislamiento por tenant. Una sesión SSO de un tenant nunca se reutiliza en otro, aunque compartan navegador: el login muestra el QR (con
prompt=none, el Verifier respondelogin_required). - La reutilización solo se intenta con la petición ya validada. El Verifier comprueba el cliente, el
redirect_uriregistrado, el scope y, si lo hay, la firma del request object antes de decidir si reutiliza la sesión, así que una petición mal formada nunca obtiene uncodepor SSO. - Los errores de una aplicación no afectan a las demás. Si tu aplicación no está en el catálogo o envía una petición mal formada, la sesión SSO del usuario sigue intacta para el resto.