September 10, 2023

Keycloak, User Federation from LDAP and Lesson Learned

LDAP

We are going to use OpenLDAP and setup in previous blog OpenLDAP Image and Custom LDIF with User and Group.

$ podman run -d --name openldap \
    -e LDAP_ROOT=dc=magnuskkarlsson,dc=se \
    -e LDAP_ADMIN_USERNAME=admin \
    -e LDAP_ADMIN_PASSWORD=changeit \
    -e LDAP_CONFIG_ADMIN_ENABLED=true \
    -e LDAP_ALLOW_ANON_BINDING=false \
    -e LDAP_CUSTOM_LDIF_DIR=/ldifs \
    -p 1389:1389 \
    -p 1636:1636 \
    -v ./ldifs:/ldifs:Z \
    docker.io/bitnami/openldap:2.6

Test it is working.

$ ldapsearch -H ldap://localhost:1389 -D cn=admin,dc=magnuskkarlsson,dc=se -w changeit -b dc=magnuskkarlsson,dc=se -s sub

Keycloak Installation

Here we will use the supported Keycloak version - RH SSO.

$ unzip rh-sso-7.6.0-server-dist.zip
$ mv rh-sso-7.6 rh-sso-7.6.0
$ cd rh-sso-7.6.0/bin/

$ ./add-user-keycloak.sh --user admin --password admin

Configure LDAP logging in RH SSO

$ vim rh-sso-7.6.0/standalone/configuration/standalone.xml 
...
    <profile>
        <subsystem xmlns="urn:jboss:domain:logging:8.0">
...
            </logger>
            <logger category="org.keycloak.storage.ldap">
                <level name="TRACE"/>
            </logger>            
            <root-logger>
...

Start

$ export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk

$ ./standalone.sh -Djboss.socket.binding.port-offset=100

Kecloak Configuration User Federation with NO Import

We will start federate openldap user and roles and not import them and test what happens, to understand how user federation works.

Add User federation

Edit mode: READ_ONLY

Import Users: OFF

No Synchronization

Add Mappers:

  • password: user-attribute-ldap-mapper; password; userPassword
  • role-ldap-mapper: role-ldap-mapper;

We can verify that user is not imported, by clicking on User and View all users

Now test federation, by logging in to user portal: http://localhost:8180/auth/realms/demo/account/

john
bitnami1

kate
bitnami1

And observe server log, that user is fetch by RDN LDAP attribute: uid

LdapOperation: search
 baseDn: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(uid=john)(objectclass=inetOrgPerson)(objectclass=organizationalPerson))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
 resultSize: 1
took: 2 ms

LdapOperation: search
 baseDn: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(uid=john)(objectclass=inetOrgPerson)(objectclass=organizationalPerson))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
 resultSize: 1
took: 1 ms

LdapOperation: search
 baseDn: ou=Groups,dc=magnuskkarlsson,dc=se
 filter: (&(member=cn=john,ou=People,dc=magnuskkarlsson,dc=se)(objectclass=groupOfNames))
 searchScope: 1
 returningAttrs: [cn]
 resultSize: 2
took: 1 ms
bitnami1

Kecloak Configuration User Federation with Import

Now Import Users: ON

Then login (user fetced by RDN LDAP attribute: uid), close private window and login again. Now is the user fetched by UUID LDAP attribute: entryUUID

LdapOperation: search
 baseDn: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(uid=john)(objectclass=inetOrgPerson)(objectclass=organizationalPerson))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
 resultSize: 1
took: 2 ms

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 3 ms

LdapOperation: search
 baseDn: ou=Groups,dc=magnuskkarlsson,dc=se
 filter: (&(member=cn=john,ou=People,dc=magnuskkarlsson,dc=se)(objectclass=groupOfNames))
 searchScope: 1
 returningAttrs: [cn]
 resultSize: 2
took: 1 ms

...

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 4 ms

When User is imported you can search and find User in Web Console and you can see in server.log that User is updated from LDAP.

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 2 ms

LdapOperation: lookupById
 baseDN: ou=People,dc=magnuskkarlsson,dc=se
 filter: (&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))
 searchScope: 1
 returningAttrs: [uid, userPassword, modifyTimestamp, cn, mail, createTimestamp, sn]
took: 1 ms

Side effects

This online dependency has also consequence, if LDAP server is offline than User cannot log in.

But if the LDAP server comes back online, User can login again.

15:04:09,715 WARN  [org.keycloak.services] (default task-44) KC-SERVICES0013: Failed authentication: org.keycloak.models.ModelException: LDAP Query failed
...
Caused by: org.keycloak.models.ModelException: Querying of LDAP failed org.keycloak.storage.ldap.idm.query.internal.LDAPQuery@54e2e27
...
Caused by: org.keycloak.models.ModelException: Could not query server using DN [ou=People,dc=magnuskkarlsson,dc=se] and filter [(&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))]
...
Caused by: javax.naming.CommunicationException: localhost:1389 [Root exception is java.net.ConnectException: Connection refused (Connection refused)]
...
Caused by: java.net.ConnectException: Connection refused (Connection refused)
...
15:04:09,755 WARN  [org.keycloak.events] (default task-44) type=LOGIN_ERROR, realmId=demo, clientId=account-console, userId=null, ipAddress=127.0.0.1, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8180/auth/realms/demo/account/#/, code_id=618ac086-9b7f-4bd1-b8a2-0498ab2e7390, username=john, authSessionParentId=618ac086-9b7f-4bd1-b8a2-0498ab2e7390, authSessionTabId=XLmqDMVrjkc

Or if LDAP server is slow (We have set Connection and Read timeout, this is important, since default values are infinite), the User cannot login.

15:10:14,894 WARN  [org.keycloak.services] (default task-44) KC-SERVICES0013: Failed authentication: org.keycloak.models.ModelException: LDAP Query failed
...
Caused by: org.keycloak.models.ModelException: Querying of LDAP failed org.keycloak.storage.ldap.idm.query.internal.LDAPQuery@c684144
...
Caused by: org.keycloak.models.ModelException: Could not query server using DN [ou=People,dc=magnuskkarlsson,dc=se] and filter [(&(objectClass=*)(entryUUID=b8f499ce-e341-103d-9c19-dfc37858caec))]
...
Caused by: javax.naming.CommunicationException: localhost:1389 [Root exception is java.net.SocketTimeoutException: connect timed out]
...
Caused by: java.net.SocketTimeoutException: connect timed out
...
15:10:14,907 WARN  [org.keycloak.events] (default task-44) type=LOGIN_ERROR, realmId=demo, clientId=account-console, userId=null, ipAddress=127.0.0.1, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8180/auth/realms/demo/account/#/, code_id=f68f708a-82de-4c5b-957f-9c7084374922, username=john, authSessionParentId=f68f708a-82de-4c5b-957f-9c7084374922, authSessionTabId=RyZud1gnRGs

Another side effect of not periodically import all User, is that User, that have never logged in, will not be visible in RH SSO.

And another side effect, is that User in RH SSO will never be pruned, i.e. if user is deleted in LDAP, that User will never be removed from RH SSO.

And this online dependency, can not be circumvented, by disabling user federation.

Kecloak Configuration User Federation and Different Edit Modes

WRITABLE means data will be synced back to LDAP on demand.

UNSYNCED means user data will be imported, but not synced back to LDAP.

READ_ONLY is a read-only LDAP store. "You cannot change the username, email, first name, last name, and other mapped attributes. Red Hat Single Sign-On shows an error anytime a user attempts to update these fields. Password updates are not supported."

No comments: