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):
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-stoppedKC_PROXY_HEADERS: xforwarded: Keycloak se fía de las cabecerasX-Forwarded-*del proxy, pero solo si vienen deKC_PROXY_TRUSTED_ADDRESSES(la IP del proxy). El proxy debe enviarX-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_PATHSañade la CA interna, la que firma los certificados de LDAPS de los controladores.
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:
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:
- 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
- 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
- Grupos de AD: en el proveedor LDAP, pestaña Mappers › Add mapper, tipo
group-ldap-mapper: LDAP Groups DNOU=Grupos,DC=contoso,DC=local, Group Object Classesgroup, Membership LDAP Attributemember, ModeREAD_ONLY. Si hay grupos anidados, User Groups Retrieve StrategyLOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY. Después, Action › Sync LDAP groups to Keycloak. - Roles de la aplicación: en Clients › compras › Roles,
compradoryaprobador. La aplicación decide por roles, no por nombres de grupos de AD que alguien puede renombrar. - Grupo a rol: Groups › GG-Compras-Aprobadores › Role mapping › Assign role, filtro por clientes,
compras aprobador. - 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 Namegroups.
6. OTP obligatorio
- Authentication › Policies › OTP Policy: TOTP, 6 dígitos, 30 segundos, Look around window 1. Valen Microsoft Authenticator, Google Authenticator o FreeOTP.
- Authentication › Flows: duplicar
browsercomobrowser-otp. En la copia, el subflujo condicional del segundo factor (su nombre empieza por Browser - Conditional) y su OTP Form pasan a Required. - Piloto: en Clients › compras › Advanced › Authentication flow overrides, Browser Flow
browser-otp. Solo esa aplicación pide OTP. - 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.
- La URL que pide la aplicación: en la barra del navegador, el parámetro
redirect_uride.../realms/contoso/protocol/openid-connect/auth?client_id=compras&redirect_uri=.... Descodificado:http://compras.contoso.local/oidc/callback. - El log de Keycloak lo confirma:
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"- Comparar con Valid redirect URIs del cliente, carácter a carácter. La comparación es exacta:
| Lo que manda la aplicación | Lo registrado | Causa |
|---|---|---|
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/callback | Barra final o ruta nueva tras la actualización |
client_id=compras-test | compras | Configuración de otro entorno |
- 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*. - Si el error sale al cerrar sesión, es el
post_logout_redirect_uriy 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.
What you'll get
A Keycloak that provides single sign-on to internal applications with Active Directory accounts: installed in production mode behind the reverse proxy, with a realm for the company, one application over OIDC and another over SAML, directory groups turned into roles, a mandatory second factor, and the method for the most repeated error when connecting a new application: Invalid parameter: redirect_uri.
Before you start
- A Linux server with Docker (or Podman) and a PostgreSQL database for Keycloak. The guide uses version 26.8; the menus are those of the 26 admin console.
- A DNS name (
sso.contoso.local) pointing at the reverse proxy (Nginx or Apache) with a certificate from the internal CA. The proxy terminates TLS and talks to Keycloak over HTTP on the internal network. - A read-only AD service account (
CONTOSO\svc-keycloak), with a password that does not expire or one managed by the identity team, and LDAPS (636) open from the server to the domain controllers. - NTP working on the Keycloak server and on the application servers. See the note at the end.
1. Install in production mode behind the proxy
start runs in production mode: it requires a hostname and does not allow the development database. Configuration goes in KC_* variables (each --some-option is written KC_SOME_OPTION):
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-stoppedKC_PROXY_HEADERS: xforwarded: Keycloak trusts the proxy'sX-Forwarded-*headers, but only when they come fromKC_PROXY_TRUSTED_ADDRESSES(the proxy's IP). The proxy must sendX-Forwarded-Proto: https.- Port 9000 is the management port (
/health,/metrics): it is not published through the proxy. - On the proxy, only
/realms/,/resources/and/.well-known/face outwards./admin/and/realms/master/, only from the IT network. KC_TRUSTSTORE_PATHSadds the internal CA, the one that signs the domain controllers' LDAPS certificates.
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"The tmpadmin user is temporary and the console warns about it: sign in at https://sso.contoso.local/admin/, create a named administrator with OTP in the master realm, and delete tmpadmin.
2. The company realm
master is only for administering Keycloak. Applications go in their own realm:
In Realm settings: Login tab, User registration off and Forgot password off (the password is the AD one and is changed through the usual procedure); Email tab, the internal SMTP server. The token issuer becomes https://sso.contoso.local/realms/contoso and everything an application needs is at https://sso.contoso.local/realms/contoso/.well-known/openid-configuration.
3. An OIDC client and a SAML one
The purchasing app (compras.contoso.local) speaks OIDC:
- Client authentication on: it is a server-side app with a secret. The secret is in the Credentials tab and is handed to the app team through the password manager, together with the Client ID and the discovery URL.
- A pure browser app (SPA) cannot keep secrets: Client authentication off and PKCE in the app.
- Valid redirect URIs with the exact URI. A trailing
*is allowed, but*on its own opens the door to any destination.
For an app that only speaks SAML (the wiki, for example): Clients › Import client with the service provider's metadata, or Create client with Client type: SAML and its Entity ID as Client ID (https://wiki.contoso.local/saml/metadata); the ACS URL goes in the Advanced tab (Assertion Consumer Service POST Binding URL) and the NameID format in Settings. The app gets Keycloak's metadata from https://sso.contoso.local/realms/contoso/protocol/saml/descriptor.
4. Active Directory over LDAP, read-only
- READ_ONLY: Keycloak writes nothing to AD. New starters, leavers and password changes stay in the directory.
- The filter leaves out accounts disabled in AD. A leaver stops getting in as soon as the account is disabled, because the password is validated against AD at every sign-in.
- The first time, Action › Sync all users. After that, the periodic sync of changes.
5. Groups, roles and what reaches the token
- AD groups: on the LDAP provider, Mappers › Add mapper tab, type
group-ldap-mapper: LDAP Groups DNOU=Grupos,DC=contoso,DC=local, Group Object Classesgroup, Membership LDAP Attributemember, ModeREAD_ONLY. With nested groups, User Groups Retrieve StrategyLOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY. Then Action › Sync LDAP groups to Keycloak. - App roles: in Clients › compras › Roles,
compradorandaprobador. The app decides by roles, not by AD group names that someone may rename. - Group to role: Groups › GG-Compras-Aprobadores › Role mapping › Assign role, filter by clients,
compras aprobador. - Check: Clients › compras › Client scopes › Evaluate, pick a test user and look at Generated access token. The roles appear in
resource_access.compras.roles. If the app also wants the groups, a Group Membership mapper in the client's dedicated scope (compras-dedicated) with Token Claim Namegroups.
6. Mandatory OTP
- Authentication › Policies › OTP Policy: TOTP, 6 digits, 30 seconds, Look around window 1. Microsoft Authenticator, Google Authenticator or FreeOTP all work.
- Authentication › Flows: duplicate
browserasbrowser-otp. In the copy, the conditional second-factor subflow (its name starts with Browser - Conditional) and its OTP Form become Required. - Pilot: in Clients › compras › Advanced › Authentication flow overrides, Browser Flow
browser-otp. Only that app asks for OTP. - Everyone: on the flow, Action › Bind flow › Browser flow. Anyone without OTP will see the screen to register the phone app when they sign in.
Lost phone: Users › user › Credentials, delete the OTP credential after verifying identity through the service desk procedure; at the next sign-in they register it again.
7. The ticket: "purchasing returns Invalid parameter: redirect_uri"
After the purchasing app's upgrade, nobody can get in: Keycloak shows We are sorry... Invalid parameter: redirect_uri.
- The URL the app asks for: in the browser's address bar, the
redirect_uriparameter of.../realms/contoso/protocol/openid-connect/auth?client_id=compras&redirect_uri=.... Decoded:http://compras.contoso.local/oidc/callback. - The Keycloak log confirms it:
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"- Compare with the client's Valid redirect URIs, character by character. The comparison is exact:
| What the app sends | What is registered | Cause |
|---|---|---|
http://... | https://... | The app, behind its own proxy, does not trust X-Forwarded-Proto and thinks it is on HTTP |
https://compras01.contoso.local/... | https://compras.contoso.local/... | People reach it by the server name or by IP |
.../oidc/callback/ | .../oidc/callback | Trailing slash or a new path after the upgrade |
client_id=compras-test | compras | Configuration from another environment |
- Fix it in the right place: here the cause is the first one; the upgrade lost the setting that made the app trust the proxy, and the app team restores it. Adding the
http://URI in Keycloak would hide it and leave the authorisation code travelling unencrypted. Adding a new URI only makes sense if the app really moved address, and then with the exact URI, never*. - If the error appears on sign-out, it is the
post_logout_redirect_uri, compared against Valid post logout redirect URIs.
In the ticket: the URI received, the one registered, the cause and who changed what.
Server clocks: TOTP depends on the clock. If the Keycloak server drifts beyond what Look around window covers, correct codes are rejected. And if the app server's clock runs fast or slow, it rejects freshly issued tokens (iat in the future, exp already passed) or SAML assertions outside NotBefore/NotOnOrAfter. Before touching tolerance settings in the app, chronyc tracking or w32tm /query /status on both sides.
Next steps
- The Entra ID SSO with SAML guide does the same for SaaS apps with cloud identities; the two live side by side.
- The reverse proxy, the
X-Forwarded-*headers and certificates are in the Nginx and Apache guide. - Back up Keycloak's PostgreSQL database and export the realm (
kc.sh export) before every version upgrade.
Is something missing, or different in your environment? Get in touch and I'll fix it.
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.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.
Apúntate y llévate las 33 ideasSign up and get the 33 ideas →