Enterprise Directory Integration: Building OpenLDAP Java Clients

OpenLDAP is the most popular open-source LDAP implementation used for centralized user authentication and directory services. This guide covers complete Java client implementation for OpenLDAP operations including authentication, search, and directory management.

Architecture Overview

Java Application → JNDI/LDAP API → OpenLDAP Server → Directory Database
↑
(LDAP Operations:
Bind, Search,
Modify, Add)

Step 1: Dependencies Setup

Maven Dependencies

<!-- pom.xml -->
<dependencies>
<!-- Spring LDAP -->
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Spring Security LDAP -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<!-- UnboundID LDAP SDK (Alternative) -->
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.8</version>
</dependency>
<!-- Apache Directory API -->
<dependency>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-all</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
</dependencies>

Step 2: Configuration Classes

LDAP Configuration Properties

// src/main/java/com/company/ldap/config/LdapProperties.java
package com.company.ldap.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "ldap")
public class LdapProperties {
// Connection settings
private String url = "ldap://localhost:389";
private String baseDn = "dc=company,dc=com";
private String userDn = "cn=admin,dc=company,dc=com";
private String password = "admin";
// Connection pool settings
private int connectionTimeout = 5000;
private int readTimeout = 5000;
private int poolSize = 10;
// Search settings
private String userSearchBase = "ou=users";
private String userSearchFilter = "(uid={0})";
private String groupSearchBase = "ou=groups";
private String groupSearchFilter = "(member={0})";
private String groupRoleAttribute = "cn";
// Security
private boolean useSsl = false;
private boolean useTls = false;
private String trustStore;
private String trustStorePassword;
public String getUserDnPattern() {
return "uid={0}," + userSearchBase + "," + baseDn;
}
public String getFullUserSearchBase() {
return userSearchBase + "," + baseDn;
}
public String getFullGroupSearchBase() {
return groupSearchBase + "," + baseDn;
}
}

Spring LDAP Configuration

// src/main/java/com/company/ldap/config/LdapConfig.java
package com.company.ldap.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;
import org.springframework.ldap.pool2.factory.Pool2ContextSource;
import org.springframework.ldap.pool2.validation.DefaultDirContextValidator;
import org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy;
import java.util.HashMap;
import java.util.Map;
@Configuration
@RequiredArgsConstructor
public class LdapConfig {
private final LdapProperties ldapProperties;
@Bean
public ContextSource contextSource() {
LdapContextSource contextSource = new LdapContextSource();
Map<String, Object> config = new HashMap<>();
// LDAP connection configuration
contextSource.setUrl(ldapProperties.getUrl());
contextSource.setBase(ldapProperties.getBaseDn());
contextSource.setUserDn(ldapProperties.getUserDn());
contextSource.setPassword(ldapProperties.getPassword());
// Connection settings
config.put("java.naming.ldap.attributes.binary", "objectGUID");
config.put("java.naming.ldap.factory.socket", "com.company.ldap.CustomSSLSocketFactory");
// Timeout settings
config.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(ldapProperties.getConnectionTimeout()));
config.put("com.sun.jndi.ldap.read.timeout", String.valueOf(ldapProperties.getReadTimeout()));
// TLS/SSL configuration
if (ldapProperties.isUseSsl()) {
config.put("java.naming.security.protocol", "ssl");
} else if (ldapProperties.isUseTls()) {
config.put("java.naming.security.protocol", "tls");
}
contextSource.setBaseEnvironmentProperties(config);
contextSource.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy());
contextSource.afterPropertiesSet();
return contextSource;
}
@Bean
public ContextSource pooledContextSource() {
Pool2ContextSource poolContextSource = new Pool2ContextSource();
poolContextSource.setContextSource(contextSource());
poolContextSource.setDirContextValidator(new DefaultDirContextValidator());
poolContextSource.setTestOnBorrow(true);
poolContextSource.setTestWhileIdle(true);
poolContextSource.setMaxTotal(ldapProperties.getPoolSize());
poolContextSource.setMaxIdle(ldapProperties.getPoolSize());
return poolContextSource;
}
@Bean
public ContextSource transactionAwareContextSource() {
return new TransactionAwareContextSourceProxy(pooledContextSource());
}
@Bean
public LdapTemplate ldapTemplate() {
return new LdapTemplate(transactionAwareContextSource());
}
}

Step 3: Data Models and ODM (Object-Directory Mapping)

LDAP Entity Classes

// src/main/java/com/company/ldap/entity/LdapUser.java
package com.company.ldap.entity;
import lombok.Data;
import org.springframework.ldap.odm.annotations.*;
import javax.naming.Name;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Entry(objectClasses = {"inetOrgPerson", "organizationalPerson", "person", "top"})
public class LdapUser {
@Id
private Name dn;
@Attribute(name = "uid")
@DnAttribute(value = "uid", index = 1)
private String username;
@Attribute(name = "cn")
private String fullName;
@Attribute(name = "sn")
private String lastName;
@Attribute(name = "givenName")
private String firstName;
@Attribute(name = "mail")
private String email;
@Attribute(name = "userPassword")
private String password;
@Attribute(name = "telephoneNumber")
private String phone;
@Attribute(name = "title")
private String jobTitle;
@Attribute(name = "department")
private String department;
@Attribute(name = "employeeNumber")
private String employeeId;
@Attribute(name = "manager")
private String managerDn;
@Attribute(name = "createTimestamp")
private LocalDateTime createdDate;
@Attribute(name = "modifyTimestamp")
private LocalDateTime modifiedDate;
@Attribute(name = "pwdAccountLockedTime")
private String lockoutTime;
@Attribute(name = "pwdFailureTime")
private List<String> passwordFailureTimes;
@Transient
private List<String> memberOf;
public boolean isLocked() {
return lockoutTime != null && !lockoutTime.equals("0");
}
public String getDisplayName() {
return fullName != null ? fullName : 
(firstName != null && lastName != null ? firstName + " " + lastName : username);
}
}
// src/main/java/com/company/ldap/entity/LdapGroup.java
@Data
@Entry(objectClasses = {"groupOfNames", "top"})
public class LdapGroup {
@Id
private Name dn;
@Attribute(name = "cn")
@DnAttribute(value = "cn", index = 1)
private String name;
@Attribute(name = "description")
private String description;
@Attribute(name = "member")
private List<String> members;
@Attribute(name = "owner")
private List<String> owners;
}
// src/main/java/com/company/ldap/entity/LdapOrganization.java
@Data
@Entry(objectClasses = {"organization", "top"})
public class LdapOrganization {
@Id
private Name dn;
@Attribute(name = "o")
@DnAttribute(value = "o", index = 0)
private String organizationName;
@Attribute(name = "description")
private String description;
@Attribute(name = "businessCategory")
private String businessCategory;
@Attribute(name = "telephoneNumber")
private String phone;
@Attribute(name = "registeredAddress")
private String address;
}

Step 4: Core LDAP Service

User Service Implementation

// src/main/java/com/company/ldap/service/LdapUserService.java
package com.company.ldap.service;
import com.company.ldap.config.LdapProperties;
import com.company.ldap.entity.LdapUser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.LikeFilter;
import org.springframework.ldap.support.LdapNameBuilder;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.stereotype.Service;
import javax.naming.Name;
import javax.naming.directory.*;
import java.util.List;
import java.util.Optional;
@Service
@Slf4j
@RequiredArgsConstructor
public class LdapUserService {
private final LdapTemplate ldapTemplate;
private final LdapProperties ldapProperties;
/**
* Authenticate user against LDAP
*/
public boolean authenticate(String username, String password) {
try {
String userDn = buildUserDn(username);
ldapTemplate.getContextSource().getContext(userDn, password);
return true;
} catch (Exception e) {
log.warn("LDAP authentication failed for user: {}", username);
return false;
}
}
/**
* Find user by username
*/
public Optional<LdapUser> findByUsername(String username) {
try {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "inetOrgPerson"));
filter.and(new EqualsFilter("uid", username));
List<LdapUser> users = ldapTemplate.search(
ldapProperties.getUserSearchBase(),
filter.encode(),
new UserContextMapper()
);
return users.stream().findFirst();
} catch (Exception e) {
log.error("Error finding user by username: {}", username, e);
return Optional.empty();
}
}
/**
* Find all users
*/
public List<LdapUser> findAllUsers() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "inetOrgPerson"));
return ldapTemplate.search(
ldapProperties.getUserSearchBase(),
filter.encode(),
new UserContextMapper()
);
}
/**
* Search users by various criteria
*/
public List<LdapUser> searchUsers(String searchTerm) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "inetOrgPerson"));
OrFilter searchFilter = new OrFilter();
searchFilter.or(new LikeFilter("cn", "*" + searchTerm + "*"));
searchFilter.or(new LikeFilter("uid", "*" + searchTerm + "*"));
searchFilter.or(new LikeFilter("mail", "*" + searchTerm + "*"));
searchFilter.or(new LikeFilter("givenName", "*" + searchTerm + "*"));
searchFilter.or(new LikeFilter("sn", "*" + searchTerm + "*"));
filter.and(searchFilter);
return ldapTemplate.search(
ldapProperties.getUserSearchBase(),
filter.encode(),
new UserContextMapper()
);
}
/**
* Create new user
*/
public void createUser(LdapUser user) {
try {
Name dn = buildUserDn(user.getUsername());
DirContextAdapter context = new DirContextAdapter(dn);
mapToContext(user, context);
ldapTemplate.bind(context);
log.info("Created LDAP user: {}", user.getUsername());
} catch (Exception e) {
log.error("Failed to create LDAP user: {}", user.getUsername(), e);
throw new RuntimeException("User creation failed", e);
}
}
/**
* Update user
*/
public void updateUser(LdapUser user) {
try {
Name dn = buildUserDn(user.getUsername());
DirContextOperations context = ldapTemplate.lookupContext(dn);
mapToContext(user, context);
ldapTemplate.modifyAttributes(context);
log.info("Updated LDAP user: {}", user.getUsername());
} catch (Exception e) {
log.error("Failed to update LDAP user: {}", user.getUsername(), e);
throw new RuntimeException("User update failed", e);
}
}
/**
* Delete user
*/
public void deleteUser(String username) {
try {
Name dn = buildUserDn(username);
ldapTemplate.unbind(dn);
log.info("Deleted LDAP user: {}", username);
} catch (Exception e) {
log.error("Failed to delete LDAP user: {}", username, e);
throw new RuntimeException("User deletion failed", e);
}
}
/**
* Change user password
*/
public void changePassword(String username, String newPassword) {
try {
Name dn = buildUserDn(username);
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("userPassword", newPassword));
ldapTemplate.modifyAttributes(dn, mods);
log.info("Password changed for user: {}", username);
} catch (Exception e) {
log.error("Failed to change password for user: {}", username, e);
throw new RuntimeException("Password change failed", e);
}
}
/**
* Unlock user account
*/
public void unlockUser(String username) {
try {
Name dn = buildUserDn(username);
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("pwdAccountLockedTime", "0"));
ldapTemplate.modifyAttributes(dn, mods);
log.info("Account unlocked for user: {}", username);
} catch (Exception e) {
log.error("Failed to unlock account for user: {}", username, e);
throw new RuntimeException("Account unlock failed", e);
}
}
/**
* Check if user exists
*/
public boolean userExists(String username) {
return findByUsername(username).isPresent();
}
/**
* Get user groups
*/
public List<String> getUserGroups(String username) {
try {
String userDn = buildUserDn(username).toString();
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "groupOfNames"));
filter.and(new EqualsFilter("member", userDn));
return ldapTemplate.search(
ldapProperties.getGroupSearchBase(),
filter.encode(),
new AbstractContextMapper<String>() {
@Override
protected String doMapFromContext(DirContextOperations ctx) {
return ctx.getStringAttribute("cn");
}
}
);
} catch (Exception e) {
log.error("Error getting groups for user: {}", username, e);
return List.of();
}
}
private Name buildUserDn(String username) {
return LdapNameBuilder.newInstance(ldapProperties.getBaseDn())
.add(ldapProperties.getUserSearchBase())
.add("uid", username)
.build();
}
private void mapToContext(LdapUser user, DirContextOperations context) {
context.setAttributeValues("objectclass", new String[] {
"top", "person", "organizationalPerson", "inetOrgPerson"
});
context.setAttributeValue("uid", user.getUsername());
context.setAttributeValue("cn", user.getFullName());
context.setAttributeValue("sn", user.getLastName());
context.setAttributeValue("givenName", user.getFirstName());
context.setAttributeValue("mail", user.getEmail());
if (user.getPassword() != null) {
context.setAttributeValue("userPassword", user.getPassword());
}
if (user.getPhone() != null) {
context.setAttributeValue("telephoneNumber", user.getPhone());
}
if (user.getJobTitle() != null) {
context.setAttributeValue("title", user.getJobTitle());
}
if (user.getDepartment() != null) {
context.setAttributeValue("department", user.getDepartment());
}
if (user.getEmployeeId() != null) {
context.setAttributeValue("employeeNumber", user.getEmployeeId());
}
}
private static class UserContextMapper extends AbstractContextMapper<LdapUser> {
@Override
protected LdapUser doMapFromContext(DirContextOperations context) {
LdapUser user = new LdapUser();
user.setDn(context.getDn());
user.setUsername(context.getStringAttribute("uid"));
user.setFullName(context.getStringAttribute("cn"));
user.setLastName(context.getStringAttribute("sn"));
user.setFirstName(context.getStringAttribute("givenName"));
user.setEmail(context.getStringAttribute("mail"));
user.setPhone(context.getStringAttribute("telephoneNumber"));
user.setJobTitle(context.getStringAttribute("title"));
user.setDepartment(context.getStringAttribute("department"));
user.setEmployeeId(context.getStringAttribute("employeeNumber"));
user.setManagerDn(context.getStringAttribute("manager"));
return user;
}
}
}

Group Service Implementation

// src/main/java/com/company/ldap/service/LdapGroupService.java
package com.company.ldap.service;
import com.company.ldap.config.LdapProperties;
import com.company.ldap.entity.LdapGroup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.support.LdapNameBuilder;
import org.springframework.stereotype.Service;
import javax.naming.Name;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import java.util.List;
import java.util.Optional;
@Service
@Slf4j
@RequiredArgsConstructor
public class LdapGroupService {
private final LdapTemplate ldapTemplate;
private final LdapProperties ldapProperties;
public Optional<LdapGroup> findByName(String groupName) {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "groupOfNames"));
filter.and(new EqualsFilter("cn", groupName));
List<LdapGroup> groups = ldapTemplate.search(
ldapProperties.getGroupSearchBase(),
filter.encode(),
new GroupContextMapper()
);
return groups.stream().findFirst();
}
public List<LdapGroup> findAllGroups() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "groupOfNames"));
return ldapTemplate.search(
ldapProperties.getGroupSearchBase(),
filter.encode(),
new GroupContextMapper()
);
}
public void createGroup(LdapGroup group) {
Name dn = buildGroupDn(group.getName());
DirContextAdapter context = new DirContextAdapter(dn);
context.setAttributeValues("objectclass", 
new String[] { "top", "groupOfNames" });
context.setAttributeValue("cn", group.getName());
if (group.getDescription() != null) {
context.setAttributeValue("description", group.getDescription());
}
// Group must have at least one member
if (group.getMembers() == null || group.getMembers().isEmpty()) {
throw new IllegalArgumentException("Group must have at least one member");
}
context.setAttributeValue("member", group.getMembers().toArray(new String[0]));
ldapTemplate.bind(context);
log.info("Created LDAP group: {}", group.getName());
}
public void addUserToGroup(String groupName, String userDn) {
try {
Name groupDn = buildGroupDn(groupName);
ModificationItem mod = new ModificationItem(DirContext.ADD_ATTRIBUTE,
new javax.naming.directory.BasicAttribute("member", userDn));
ldapTemplate.modifyAttributes(groupDn, new ModificationItem[] { mod });
log.info("Added user {} to group {}", userDn, groupName);
} catch (Exception e) {
log.error("Failed to add user to group", e);
throw new RuntimeException("Failed to add user to group", e);
}
}
public void removeUserFromGroup(String groupName, String userDn) {
try {
Name groupDn = buildGroupDn(groupName);
ModificationItem mod = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
new javax.naming.directory.BasicAttribute("member", userDn));
ldapTemplate.modifyAttributes(groupDn, new ModificationItem[] { mod });
log.info("Removed user {} from group {}", userDn, groupName);
} catch (Exception e) {
log.error("Failed to remove user from group", e);
throw new RuntimeException("Failed to remove user from group", e);
}
}
public void deleteGroup(String groupName) {
Name dn = buildGroupDn(groupName);
ldapTemplate.unbind(dn);
log.info("Deleted LDAP group: {}", groupName);
}
private Name buildGroupDn(String groupName) {
return LdapNameBuilder.newInstance(ldapProperties.getBaseDn())
.add(ldapProperties.getGroupSearchBase())
.add("cn", groupName)
.build();
}
private static class GroupContextMapper extends org.springframework.ldap.core.AbstractContextMapper<LdapGroup> {
@Override
protected LdapGroup doMapFromContext(org.springframework.ldap.core.DirContextOperations context) {
LdapGroup group = new LdapGroup();
group.setDn(context.getDn());
group.setName(context.getStringAttribute("cn"));
group.setDescription(context.getStringAttribute("description"));
Object members = context.getObjectAttribute("member");
if (members != null) {
if (members instanceof String) {
group.setMembers(List.of((String) members));
} else if (members instanceof String[]) {
group.setMembers(List.of((String[]) members));
}
}
return group;
}
}
}

Step 5: Advanced LDAP Operations

LDAP Search Service

// src/main/java/com/company/ldap/service/LdapSearchService.java
package com.company.ldap.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.CountNameClassPairCallbackHandler;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.OrFilter;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.stereotype.Service;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.LdapContext;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
@RequiredArgsConstructor
public class LdapSearchService {
private final LdapTemplate ldapTemplate;
/**
* Count entries matching filter
*/
public int countEntries(String searchBase, String filter) {
CountNameClassPairCallbackHandler handler = new CountNameClassPairCallbackHandler();
ldapTemplate.search(searchBase, filter, SearchControls.OBJECT_SCOPE, 
null, handler);
return handler.getNoOfRows();
}
/**
* Advanced search with custom controls
*/
public <T> List<T> advancedSearch(String searchBase, String filter, 
String[] returningAttributes,
org.springframework.ldap.core.ContextMapper<T> mapper) {
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningObjFlag(true);
if (returningAttributes != null) {
controls.setReturningAttributes(returningAttributes);
}
return ldapTemplate.search(searchBase, filter, controls, mapper);
}
/**
* Paged search for large directories
*/
public <T> List<T> pagedSearch(String searchBase, String filter, 
int pageSize, org.springframework.ldap.core.ContextMapper<T> mapper) {
List<T> results = new ArrayList<>();
byte[] cookie = null;
do {
LdapTemplate pagedLdapTemplate = new LdapTemplate(ldapTemplate.getContextSource());
pagedLdapTemplate.setIgnorePartialResultException(true);
// This would need custom implementation for paged results
// using DirContextProcessor
} while (cookie != null);
return results;
}
/**
* Check LDAP server health
*/
public boolean isServerHealthy() {
try {
LdapContext context = (LdapContext) ldapTemplate.getContextSource().getContext();
context.close();
return true;
} catch (Exception e) {
log.error("LDAP server health check failed", e);
return false;
}
}
/**
* Get directory statistics
*/
public DirectoryStats getDirectoryStats() {
DirectoryStats stats = new DirectoryStats();
// Count users
AndFilter userFilter = new AndFilter();
userFilter.and(new EqualsFilter("objectclass", "inetOrgPerson"));
stats.setUserCount(countEntries("", userFilter.encode()));
// Count groups
AndFilter groupFilter = new AndFilter();
groupFilter.and(new EqualsFilter("objectclass", "groupOfNames"));
stats.setGroupCount(countEntries("", groupFilter.encode()));
return stats;
}
@Data
public static class DirectoryStats {
private int userCount;
private int groupCount;
private int organizationCount;
}
}

Step 6: Spring Security Integration

LDAP Authentication Provider

// src/main/java/com/company/ldap/security/LdapSecurityConfig.java
package com.company.ldap.security;
import com.company.ldap.config.LdapProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class LdapSecurityConfig {
private final LdapProperties ldapProperties;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.permitAll()
);
return http.build();
}
@Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = 
new ActiveDirectoryLdapAuthenticationProvider(
ldapProperties.getBaseDn(), 
ldapProperties.getUrl()
);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
@Bean
public LdapAuthoritiesPopulator ldapAuthoritiesPopulator() {
return new CustomLdapAuthoritiesPopulator();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Custom Authorities Populator

// src/main/java/com/company/ldap/security/CustomLdapAuthoritiesPopulator.java
package com.company.ldap.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Component
@Slf4j
@RequiredArgsConstructor
public class CustomLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
private final LdapGroupService groupService;
@Override
public Collection<? extends GrantedAuthority> getGrantedAuthorities(
DirContextOperations userData, String username) {
Set<GrantedAuthority> authorities = new HashSet<>();
try {
// Add default role
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
// Get groups from LDAP
Collection<String> groups = groupService.getUserGroups(username);
for (String group : groups) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + group.toUpperCase()));
// Map specific groups to roles
if ("admin".equalsIgnoreCase(group) || "administrators".equalsIgnoreCase(group)) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
}
log.debug("User {} has authorities: {}", username, authorities);
} catch (Exception e) {
log.error("Failed to get authorities for user: {}", username, e);
}
return authorities;
}
}

Step 7: REST API Controllers

// src/main/java/com/company/ldap/controller/LdapUserController.java
package com.company.ldap.controller;
import com.company.ldap.entity.LdapUser;
import com.company.ldap.service.LdapUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/ldap/users")
@RequiredArgsConstructor
@Slf4j
public class LdapUserController {
private final LdapUserService ldapUserService;
@GetMapping
public ResponseEntity<List<LdapUser>> getAllUsers() {
try {
List<LdapUser> users = ldapUserService.findAllUsers();
return ResponseEntity.ok(users);
} catch (Exception e) {
log.error("Failed to get users", e);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/{username}")
public ResponseEntity<LdapUser> getUser(@PathVariable String username) {
try {
Optional<LdapUser> user = ldapUserService.findByUsername(username);
return user.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
} catch (Exception e) {
log.error("Failed to get user: {}", username, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping
public ResponseEntity<LdapUser> createUser(@RequestBody LdapUser user) {
try {
if (ldapUserService.userExists(user.getUsername())) {
return ResponseEntity.badRequest().build();
}
ldapUserService.createUser(user);
return ResponseEntity.ok(user);
} catch (Exception e) {
log.error("Failed to create user: {}", user.getUsername(), e);
return ResponseEntity.internalServerError().build();
}
}
@PutMapping("/{username}")
public ResponseEntity<LdapUser> updateUser(@PathVariable String username, 
@RequestBody LdapUser user) {
try {
if (!ldapUserService.userExists(username)) {
return ResponseEntity.notFound().build();
}
user.setUsername(username); // Ensure username matches path
ldapUserService.updateUser(user);
return ResponseEntity.ok(user);
} catch (Exception e) {
log.error("Failed to update user: {}", username, e);
return ResponseEntity.internalServerError().build();
}
}
@DeleteMapping("/{username}")
public ResponseEntity<Void> deleteUser(@PathVariable String username) {
try {
if (!ldapUserService.userExists(username)) {
return ResponseEntity.notFound().build();
}
ldapUserService.deleteUser(username);
return ResponseEntity.ok().build();
} catch (Exception e) {
log.error("Failed to delete user: {}", username, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/{username}/authenticate")
public ResponseEntity<Boolean> authenticateUser(@PathVariable String username, 
@RequestBody AuthenticationRequest request) {
try {
boolean authenticated = ldapUserService.authenticate(username, request.getPassword());
return ResponseEntity.ok(authenticated);
} catch (Exception e) {
log.error("Authentication failed for user: {}", username, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/{username}/change-password")
public ResponseEntity<Void> changePassword(@PathVariable String username,
@RequestBody ChangePasswordRequest request) {
try {
ldapUserService.changePassword(username, request.getNewPassword());
return ResponseEntity.ok().build();
} catch (Exception e) {
log.error("Password change failed for user: {}", username, e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/{username}/unlock")
public ResponseEntity<Void> unlockUser(@PathVariable String username) {
try {
ldapUserService.unlockUser(username);
return ResponseEntity.ok().build();
} catch (Exception e) {
log.error("Failed to unlock user: {}", username, e);
return ResponseEntity.internalServerError().build();
}
}
@GetMapping("/{username}/groups")
public ResponseEntity<List<String>> getUserGroups(@PathVariable String username) {
try {
List<String> groups = ldapUserService.getUserGroups(username);
return ResponseEntity.ok(groups);
} catch (Exception e) {
log.error("Failed to get groups for user: {}", username, e);
return ResponseEntity.internalServerError().build();
}
}
@Data
public static class AuthenticationRequest {
private String password;
}
@Data
public static class ChangePasswordRequest {
private String newPassword;
}
}

Step 8: Application Configuration

application.yml

# application.yml
spring:
ldap:
urls: ldap://localhost:389
base: dc=company,dc=com
username: cn=admin,dc=company,dc=com
password: admin
base-environment:
java.naming.ldap.attributes.binary: objectGUID
pooled: true
ldap:
url: ldap://localhost:389
base-dn: dc=company,dc=com
user-dn: cn=admin,dc=company,dc=com
password: admin
user-search-base: ou=users
user-search-filter: (uid={0})
group-search-base: ou=groups
group-search-filter: (member={0})
connection-timeout: 5000
read-timeout: 5000
pool-size: 10
server:
port: 8080
logging:
level:
com.company.ldap: DEBUG
org.springframework.ldap: INFO
org.springframework.security: INFO

Best Practices

  1. Connection Management
  • Use connection pooling
  • Implement proper timeout settings
  • Handle connection failures gracefully
  1. Security
  • Use TLS/SSL for production
  • Validate all inputs to prevent LDAP injection
  • Implement proper access controls
  • Secure credential storage
  1. Performance
  • Use paged results for large directories
  • Cache frequently accessed data
  • Implement search filters efficiently
  1. Error Handling
  • Handle LDAP exceptions appropriately
  • Provide meaningful error messages
  • Implement retry mechanisms for transient failures

Conclusion

This OpenLDAP Java client implementation provides:

  • Complete CRUD Operations: User and group management
  • Authentication: Secure LDAP authentication
  • Search Capabilities: Advanced search with filtering
  • Security Integration: Spring Security compatibility
  • Performance: Connection pooling and efficient operations

Key implementation aspects:

  1. Configuration management for LDAP connection
  2. Object-Directory Mapping for entity management
  3. Comprehensive service layer for business logic
  4. REST API for external integration
  5. Security integration for authentication and authorization

This solution is production-ready and can be extended with additional features like replication monitoring, schema management, and advanced directory operations.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper