GuíasGuides · Nivel 3Level 3

Keycloak: SSO para aplicaciones internas con un realm, clientes OIDC y SAML, Active Directory por LDAP, roles, OTP y un invalid_redirect_uriKeycloak: SSO for internal applications with a realm, OIDC and SAML clients, Active Directory over LDAP, roles, OTP and an 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.Keycloak 26 in a container and in production mode behind a reverse proxy, its own realm, an OIDC and a SAML client, read-only Active Directory over LDAP, groups and roles in the token, mandatory OTP, and an app that returns Invalid parameter: redirect_uri, with a note on server clocks.

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.

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):

/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 trusts the proxy's X-Forwarded-* headers, but only when they come from KC_PROXY_TRUSTED_ADDRESSES (the proxy's IP). The proxy must send X-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_PATHS adds the internal CA, the one that signs the domain controllers' LDAPS certificates.
Server · start and check
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:

Admin console · Manage realms
Manage realmsCreate realm
Realm namecontoso
Enabled
Create

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:

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 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

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 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

  1. AD groups: on the LDAP provider, Mappers › Add mapper tab, type group-ldap-mapper: LDAP Groups DN OU=Grupos,DC=contoso,DC=local, Group Object Classes group, Membership LDAP Attribute member, Mode READ_ONLY. With nested groups, User Groups Retrieve Strategy LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY. Then Action › Sync LDAP groups to Keycloak.
  2. App roles: in Clients › compras › Roles, comprador and aprobador. The app decides by roles, not by AD group names that someone may rename.
  3. Group to role: Groups › GG-Compras-Aprobadores › Role mapping › Assign role, filter by clients, compras aprobador.
  4. 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 Name groups.

6. Mandatory OTP

  1. Authentication › Policies › OTP Policy: TOTP, 6 digits, 30 seconds, Look around window 1. Microsoft Authenticator, Google Authenticator or FreeOTP all work.
  2. Authentication › Flows: duplicate browser as browser-otp. In the copy, the conditional second-factor subflow (its name starts with Browser - Conditional) and its OTP Form become Required.
  3. Pilot: in Clients › compras › Advanced › Authentication flow overrides, Browser Flow browser-otp. Only that app asks for OTP.
  4. 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.

  1. The URL the app asks for: in the browser's address bar, the redirect_uri parameter of .../realms/contoso/protocol/openid-connect/auth?client_id=compras&redirect_uri=.... Decoded: http://compras.contoso.local/oidc/callback.
  2. The Keycloak log confirms it:
Server · error events
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. Compare with the client's Valid redirect URIs, character by character. The comparison is exact:
What the app sendsWhat is registeredCause
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/callbackTrailing slash or a new path after the upgrade
client_id=compras-testcomprasConfiguration from another environment
  1. 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 *.
  2. 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 →