Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/documentation/release_notes/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ include::topics/templates/release-header.adoc[]
== {project_name_full} 26.5.0
include::topics/26_5_0.adoc[leveloffset=2]

== {project_name_full} 26.4.6
include::topics/26_4_6.adoc[leveloffset=2]

== {project_name_full} 26.4.0
include::topics/26_4_0.adoc[leveloffset=2]

Expand Down
9 changes: 9 additions & 0 deletions docs/documentation/release_notes/topics/26_4_6.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Release notes should contain only headline-worthy new features,
// assuming that people who migrate will read the upgrading guide anyway.

This release adds filtering of LDAP referrals by default.
This change enhances security and aligns with best practices for LDAP configurations.

If you can not upgrade to this release yet, we recommend disabling LDAP referrals in all LDAP providers in all of your realms.

For detailed upgrade instructions, https://www.keycloak.org/docs/latest/upgrading/index.html[review the upgrading guide].
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.naming.NamingException;
import javax.naming.spi.NamingManager;

import org.keycloak.Config;
import org.keycloak.common.constants.KerberosConstants;
Expand Down Expand Up @@ -85,6 +87,8 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
private static final Logger logger = Logger.getLogger(LDAPStorageProviderFactory.class);
public static final String PROVIDER_NAME = LDAPConstants.LDAP_PROVIDER;
private static final String LDAP_CONNECTION_POOL_PROTOCOL = "com.sun.jndi.ldap.connect.pool.protocol";
private static final String SECURE_REFERRAL = "secureReferral";
private static final boolean SECURE_REFERRAL_DEFAULT = true;

private LDAPIdentityStoreRegistry ldapStoreRegistry;

Expand Down Expand Up @@ -302,13 +306,36 @@ public void validateConfiguration(KeycloakSession session, RealmModel realm, Com

@Override
public void init(Config.Scope config) {
if (config.getBoolean(SECURE_REFERRAL, SECURE_REFERRAL_DEFAULT)) {
setObjectFactoryBuilder();
} else {
logger.warnf("Insecure LDAP referrals are enabled. The option 'secure-referral' is deprecated and it will be removed in future releases.");
}

// set connection pooling for plain and tls protocols by default
if (System.getProperty(LDAP_CONNECTION_POOL_PROTOCOL) == null) {
System.setProperty(LDAP_CONNECTION_POOL_PROTOCOL, "plain ssl");
}

this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
}

@Override
public List<ProviderConfigProperty> getConfigMetadata() {

ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create();

builder.property()
.name(SECURE_REFERRAL)
.type("boolean")
.helpText("Allow only secure LDAP referrals (deprecated)")
.defaultValue(SECURE_REFERRAL_DEFAULT)
.add();

return builder.build();

}

@Override
public void close() {
this.ldapStoreRegistry = null;
Expand Down Expand Up @@ -728,4 +755,15 @@ protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAu
return new KerberosUsernamePasswordAuthenticator(kerberosConfig);
}

private void setObjectFactoryBuilder() {
try {
NamingManager.setObjectFactoryBuilder(new ObjectFactoryBuilder());
} catch (NamingException | IllegalStateException e) {
if (e instanceof IllegalStateException && ObjectFactoryBuilder.isSet()) {
return;
}

throw new RuntimeException("Failed to set the server JNDI ObjectFactoryBuilder", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.keycloak.storage.ldap;

import java.util.Hashtable;
import java.util.List;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.ldap.LdapContext;
import javax.naming.spi.NamingManager;
import javax.naming.spi.ObjectFactory;

import org.keycloak.storage.ldap.idm.store.ldap.SessionBoundInitialLdapContext;
import org.keycloak.utils.KeycloakSessionUtil;

import org.jboss.logging.Logger;

/**
* <p>A {@link javax.naming.spi.ObjectFactoryBuilder} implementation to filter out referral references if they do not
* point to an LDAP URL.
*
* <p>When the LDAP provider encounters a referral, it tries to create an {@link ObjectFactory} from this builder.
* If the referral reference contains an LDAP URL, a {@link DirContextObjectFactory} is created to handle the referral.
* Otherwise, a {@link CommunicationException} is thrown to indicate that the referral cannot be processed.
*/
final class ObjectFactoryBuilder implements javax.naming.spi.ObjectFactoryBuilder, ObjectFactory {

private static final Logger logger = Logger.getLogger(ObjectFactoryBuilder.class);
private static final String IS_KC_OBJECT_FACTORY_BUILDER = "kc.jndi.object.factory.builder";

static boolean isSet() {
Hashtable<Object, Object> env = new Hashtable<>();

env.put(ObjectFactoryBuilder.IS_KC_OBJECT_FACTORY_BUILDER, Boolean.TRUE);

try {
Object instance = NamingManager.getObjectInstance(null, null, null, env);

if (instance != null && instance.getClass().getName().equals(ObjectFactoryBuilder.class.getName())) {
return true;
}
} catch (Exception e) {
throw new RuntimeException("Failed to determine if ObjectFactoryBuilder is set", e);
}

return false;
}

@Override
public ObjectFactory createObjectFactory(Object obj, Hashtable<?, ?> environment) throws NamingException {
if (logger.isTraceEnabled()) {
logger.tracef("Creating ObjectFactory for object: %s", obj);
}

if (obj instanceof Reference ref) {
String factoryClassName = ref.getFactoryClassName();

if (factoryClassName != null) {
logger.warnf("Referral refence contains an object factory %s but it will be ignored", factoryClassName);
}

String ldapUrl = getLdapUrl(ref);

if (ldapUrl != null) {
return new DirContextObjectFactory(ldapUrl);
}
} else {
logger.debugf("Unsupported reference object of type %s: ", obj);
return this;
}

throw new CommunicationException("Referral reference does not contain an LDAP URL: " + obj);
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> env) {
if (env != null && env.containsKey(IS_KC_OBJECT_FACTORY_BUILDER)) {
return this;
}
return obj;
}

private String getLdapUrl(Reference ref) {
for (int i = 0; i < ref.size(); i++) {
RefAddr addr = ref.get(i);
String addrType = addr.getType();

if ("URL".equalsIgnoreCase(addrType)) {
Object content = addr.getContent();

if (content == null) {
return null;
}

String rawUrl = content.toString();

for (String url : List.of(rawUrl.split(" "))) {
if (!url.toLowerCase().startsWith("ldap")) {
logger.warnf("Unsupported scheme from reference URL %s. Ignoring reference.", url);
return null;
}
}

return rawUrl;
} else {
logger.warnf("Ignoring address of type '%s' from referral reference", addrType);
}
}

return null;
}

private record DirContextObjectFactory(String ldapUrl) implements ObjectFactory {

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> env) throws Exception {
@SuppressWarnings("unchecked")
Hashtable<Object, Object> newEnv = (Hashtable<Object, Object>) env.clone();
newEnv.put(LdapContext.PROVIDER_URL, ldapUrl);
return new SessionBoundInitialLdapContext(KeycloakSessionUtil.getKeycloakSession(), newEnv, null);
}
}
}
Loading