Guías · Nivel 3

Keycloak: SSO para aplicaciones internas con un realm, clientes OIDC y SAML, Active Directory por LDAP, roles, OTP y un invalid_redirect_uri

Keycloak 26 en contenedor y en modo producción detrás de un proxy inverso, un realm propio, un cliente OIDC y uno SAML, Active Directory en solo lectura por LDAP, grupos y roles en el token, OTP obligatorio y una aplicación que devuelve Invalid parameter: redirect_uri, con una nota sobre la hora de los servidores.

Qué vas a conseguir

Un Keycloak que da inicio de sesión único a las aplicaciones internas con las cuentas de Active Directory: instalado en modo producción detrás del proxy inverso, con un realm para la empresa, una aplicación por OIDC y otra por SAML, los grupos del directorio convertidos en roles, un segundo factor obligatorio y el método para el error más repetido al conectar una aplicación nueva: Invalid parameter: redirect_uri.

Antes de empezar

  • Un servidor Linux con Docker (o Podman) y una base de datos PostgreSQL para Keycloak. La guía usa la versión 26.8; los menús son los de la consola de administración de la 26.
  • Un nombre DNS (sso.contoso.local) que apunte al proxy inverso (Nginx o Apache) con un certificado de la CA interna. El proxy termina TLS y habla con Keycloak por HTTP en la red interna.
  • Una cuenta de servicio de AD solo para lectura (CONTOSO\svc-keycloak), con contraseña que no caduque o gestionada por el equipo de identidad, y LDAPS (636) abierto desde el servidor hacia los controladores de dominio.
  • NTP funcionando en el servidor de Keycloak y en los de las aplicaciones. Ver la nota del final.

1. Instalar en modo producción detrás del proxy

start arranca en modo producción: exige un hostname y no admite la base de datos de desarrollo. La configuración va en variables KC_* (cada opción --algo-asi se escribe KC_ALGO_ASI):

/opt/keycloak/compose.yaml
services:  keycloak:    image: quay.io/keycloak/keycloak:26.8.1    container_name: keycloak    command: start    environment:      KC_DB: postgres      KC_DB_URL: jdbc:postgresql://db01.contoso.local:5432/keycloak      KC_DB_USERNAME: keycloak      KC_DB_PASSWORD: ${KC_DB_PASSWORD}      KC_HOSTNAME: https://sso.contoso.local      KC_HTTP_ENABLED: "true"      KC_PROXY_HEADERS: xforwarded      KC_PROXY_TRUSTED_ADDRESSES: 10.20.0.10      KC_HEALTH_ENABLED: "true"      KC_TRUSTSTORE_PATHS: /opt/keycloak/conf/truststores/contoso-root.pem      KC_BOOTSTRAP_ADMIN_USERNAME: tmpadmin      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD}    volumes:      - ./contoso-root.pem:/opt/keycloak/conf/truststores/contoso-root.pem:ro    ports:      - "10.20.0.21:8080:8080"      - "127.0.0.1:9000:9000"    restart: unless-stopped
  • KC_PROXY_HEADERS: xforwarded: Keycloak se fía de las cabeceras X-Forwarded-* del proxy, pero solo si vienen de KC_PROXY_TRUSTED_ADDRESSES (la IP del proxy). El proxy debe enviar X-Forwarded-Proto: https.
  • El puerto 9000 es el de gestión (/health, /metrics): no se publica por el proxy.
  • En el proxy, hacia fuera solo /realms/, /resources/ y /.well-known/. /admin/ y /realms/master/, solo desde la red de IT.
  • KC_TRUSTSTORE_PATHS añade la CA interna, la que firma los certificados de LDAPS de los controladores.
Servidor · arrancar y comprobar
docker compose up -ddocker logs keycloak 2>&1 | grep -o "Keycloak 26.8.1 on JVM"Keycloak 26.8.1 on JVMcurl -s http://127.0.0.1:9000/health/ready | grep -o '"status": "UP"' | head -1"status": "UP"

El usuario tmpadmin es temporal y la consola lo avisa: entra en https://sso.contoso.local/admin/, crea en el realm master un administrador con nombre propio y OTP, y borra tmpadmin.

2. El realm de la empresa

master es solo para administrar Keycloak. Las aplicaciones van en un realm propio:

Consola de administración · Manage realms
Manage realmsCreate realm
Realm namecontoso
Enabled
Create

En Realm settings: pestaña Login, User registration desactivado y Forgot password desactivado (la contraseña es la de AD y se cambia por el procedimiento de siempre); pestaña Email, el servidor SMTP interno. El emisor de los tokens queda como https://sso.contoso.local/realms/contoso y todo lo que una aplicación necesita está en https://sso.contoso.local/realms/contoso/.well-known/openid-configuration.

3. Un cliente OIDC y uno SAML

La aplicación de compras (compras.contoso.local) habla OIDC:

Realm contoso · Clients › Create client
Client typeOpenID Connect
Client IDcompras
Next
Client authentication
Standard flow
Direct access grants
Next
Root URLhttps://compras.contoso.local
Valid redirect URIshttps://compras.contoso.local/oidc/callback
Valid post logout redirect URIshttps://compras.contoso.local/
Web origins+
Save
  • Client authentication activado: es una aplicación de servidor con secreto. El secreto está en la pestaña Credentials y se entrega al equipo de la aplicación por el gestor de contraseñas, junto con el Client ID y la URL de descubrimiento.
  • Una aplicación de navegador pura (SPA) no puede guardar secretos: Client authentication desactivado y PKCE en la aplicación.
  • Valid redirect URIs con la URI exacta. Un * al final se admite, pero * solo abre la puerta a cualquier destino.

Para una aplicación que solo habla SAML (la wiki, por ejemplo): Clients › Import client con los metadatos del proveedor de servicio, o Create client con Client type: SAML y como Client ID su Entity ID (https://wiki.contoso.local/saml/metadata); la URL de ACS va en la pestaña Advanced (Assertion Consumer Service POST Binding URL) y el formato del NameID, en Settings. La aplicación recibe los metadatos de Keycloak en https://sso.contoso.local/realms/contoso/protocol/saml/descriptor.

4. Active Directory por LDAP, en solo lectura

Realm contoso · User federation › Add Ldap providers
VendorActive Directory
Connection URLldaps://dc01.contoso.local:636
Bind typesimple
Bind DNCN=svc-keycloak,OU=Servicios,DC=contoso,DC=local
Bind credentials••••••••
Test connection
Test authentication
Edit modeREAD_ONLY
Users DNOU=Usuarios,DC=contoso,DC=local
Username LDAP attributesAMAccountName
RDN LDAP attributecn
UUID LDAP attributeobjectGUID
User object classesperson, organizationalPerson, user
User LDAP filter(!(userAccountControl:1.2.840.113556.1.4.803:=2))
Search scopeSubtree
Import users
Periodic changed users sync: 3600
Save
  • READ_ONLY: Keycloak no escribe nada en AD. Las altas, bajas y cambios de contraseña siguen en el directorio.
  • El filtro deja fuera las cuentas deshabilitadas en AD. Una baja deja de entrar en cuanto se deshabilita, porque la contraseña se valida contra AD en cada inicio de sesión.
  • La primera vez, Action › Sync all users. Después, la sincronización periódica de cambios.

5. Grupos, roles y lo que llega en el token

  1. Grupos de AD: en el proveedor LDAP, pestaña Mappers › Add mapper, tipo group-ldap-mapper: LDAP Groups DN OU=Grupos,DC=contoso,DC=local, Group Object Classes group, Membership LDAP Attribute member, Mode READ_ONLY. Si hay grupos anidados, User Groups Retrieve Strategy LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY. Después, Action › Sync LDAP groups to Keycloak.
  2. Roles de la aplicación: en Clients › compras › Roles, comprador y aprobador. La aplicación decide por roles, no por nombres de grupos de AD que alguien puede renombrar.
  3. Grupo a rol: Groups › GG-Compras-Aprobadores › Role mapping › Assign role, filtro por clientes, compras aprobador.
  4. Comprobar: Clients › compras › Client scopes › Evaluate, elegir un usuario de prueba y ver Generated access token. Los roles salen en resource_access.compras.roles. Si la aplicación quiere también los grupos, un mapper Group Membership en el ámbito dedicado del cliente (compras-dedicated) con Token Claim Name groups.

6. OTP obligatorio

  1. Authentication › Policies › OTP Policy: TOTP, 6 dígitos, 30 segundos, Look around window 1. Valen Microsoft Authenticator, Google Authenticator o FreeOTP.
  2. Authentication › Flows: duplicar browser como browser-otp. En la copia, el subflujo condicional del segundo factor (su nombre empieza por Browser - Conditional) y su OTP Form pasan a Required.
  3. Piloto: en Clients › compras › Advanced › Authentication flow overrides, Browser Flow browser-otp. Solo esa aplicación pide OTP.
  4. Todos: en el flujo, Action › Bind flow › Browser flow. Quien no tenga OTP verá al entrar la pantalla para registrar la aplicación del móvil.

Móvil perdido: Users › usuario › Credentials, borrar la credencial OTP tras verificar la identidad por el procedimiento del service desk; en el siguiente inicio de sesión la vuelve a registrar.

7. El ticket: "compras devuelve Invalid parameter: redirect_uri"

Después de la actualización de la aplicación de compras, nadie entra: Keycloak muestra We are sorry... Invalid parameter: redirect_uri.

  1. La URL que pide la aplicación: en la barra del navegador, el parámetro redirect_uri de .../realms/contoso/protocol/openid-connect/auth?client_id=compras&redirect_uri=.... Descodificado: http://compras.contoso.local/oidc/callback.
  2. El log de Keycloak lo confirma:
Servidor · eventos de error
docker logs keycloak 2>&1 | grep invalid_redirect_uri | tail -1WARN  [org.keycloak.events] (executor-thread-12) type="LOGIN_ERROR", realmId="6f1c2e0a-...", clientId="compras", userId="null", ipAddress="10.20.5.31", error="invalid_redirect_uri", redirect_uri="http://compras.contoso.local/oidc/callback"
  1. Comparar con Valid redirect URIs del cliente, carácter a carácter. La comparación es exacta:
Lo que manda la aplicaciónLo registradoCausa
http://...https://...La aplicación, detrás de su propio proxy, no se fía de X-Forwarded-Proto y cree que está en HTTP
https://compras01.contoso.local/...https://compras.contoso.local/...Se entra por el nombre del servidor o por la IP
.../oidc/callback/.../oidc/callbackBarra final o ruta nueva tras la actualización
client_id=compras-testcomprasConfiguración de otro entorno
  1. Arreglar en el sitio correcto: aquí, la causa es la primera; la actualización ha perdido el ajuste que hacía confiar en el proxy, y el equipo de la aplicación lo restaura. Añadir la URI http:// en Keycloak lo taparía y dejaría el código de autorización viajando sin cifrar. Añadir una URI nueva solo tiene sentido si la aplicación de verdad cambió de dirección, y entonces con la URI exacta, nunca *.
  2. Si el error sale al cerrar sesión, es el post_logout_redirect_uri y se compara con Valid post logout redirect URIs.

En el ticket: la URI recibida, la registrada, la causa y quién cambió qué.

La hora de los servidores: TOTP depende del reloj. Si el servidor de Keycloak se desvía más de lo que cubre Look around window, los códigos correctos se rechazan. Y si el reloj del servidor de la aplicación va adelantado o atrasado, rechaza tokens recién emitidos (iat en el futuro, exp ya pasado) o aserciones SAML fuera de NotBefore/NotOnOrAfter. Antes de tocar márgenes de tolerancia en la aplicación, chronyc tracking o w32tm /query /status en los dos lados.

Siguientes pasos

  • La guía de Entra ID SSO con SAML hace lo mismo para aplicaciones SaaS con identidades en la nube; las dos conviven.
  • El proxy inverso, las cabeceras X-Forwarded-* y los certificados están en la de Nginx y Apache.
  • Copia de la base de datos PostgreSQL de Keycloak y exportación del realm (kc.sh export) antes de cada actualización de versión.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.

Apúntate y llévate las 33 ideas →