LDAP Authentication: Enterprise Directory Integration
LDAP in the Enterprise
LDAP (Lightweight Directory Access Protocol) is the standard protocol for directory services. Microsoft Active Directory, OpenLDAP, and many other enterprise identity providers use LDAP. If your application must authenticate users from a corporate directory, LDAP integration is the path.
sequenceDiagram
participant Client
participant SpringSecurity as Spring Security
participant LDAP as LDAP / Active Directory
Client->>SpringSecurity: POST /login {username, password}
SpringSecurity->>LDAP: Bind as service account (manager DN)
LDAP-->>SpringSecurity: Bind success
SpringSecurity->>LDAP: Search for user:\n(uid=alice,ou=users,dc=example,dc=com)
LDAP-->>SpringSecurity: User DN found
SpringSecurity->>LDAP: Bind as user (verify password)
LDAP-->>SpringSecurity: Bind success = password correct
SpringSecurity->>LDAP: Search groups for user
LDAP-->>SpringSecurity: Groups: [cn=developers, cn=devops]
SpringSecurity->>SpringSecurity: Map groups to roles
SpringSecurity-->>Client: Authentication success (ROLE_DEVELOPER, ROLE_DEVOPS)
Dependencies
<!-- Spring LDAP / Spring Security LDAP -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<!-- Embedded LDAP server for development/testing -->
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<scope>test</scope>
</dependency>
LDAP Authentication Configuration
Bind Authentication (Standard)
@Configuration
@EnableWebSecurity
public class LdapSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public LdapAuthenticationProvider ldapAuthenticationProvider() {
return new LdapAuthenticationProvider(
bindAuthenticator(),
authoritiesPopulator()
);
}
@Bean
public BindAuthenticator bindAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator(contextSource());
// Search pattern — {0} is replaced with the username
authenticator.setUserSearch(new FilterBasedLdapUserSearch(
"ou=users", // search base (relative to root)
"(uid={0})", // search filter — {0} = username
contextSource()
));
// Or use direct DN pattern (faster, no search needed)
// authenticator.setUserDnPatterns(new String[]{"uid={0},ou=users"});
return authenticator;
}
@Bean
public LdapAuthoritiesPopulator authoritiesPopulator() {
DefaultLdapAuthoritiesPopulator populator =
new DefaultLdapAuthoritiesPopulator(contextSource(), "ou=groups");
populator.setGroupSearchFilter("(member={0})"); // {0} = user's full DN
populator.setGroupRoleAttribute("cn"); // group name attribute
populator.setRolePrefix("ROLE_"); // prefix added to group name
populator.setConvertToUpperCase(true); // cn=developers → ROLE_DEVELOPERS
return populator;
}
@Bean
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource contextSource =
new DefaultSpringSecurityContextSource("ldap://ldap.example.com:389/dc=example,dc=com");
contextSource.setUserDn("cn=serviceaccount,dc=example,dc=com"); // manager DN
contextSource.setPassword("manager-password");
return contextSource;
}
}
Active Directory (Special Case)
Active Directory uses a different LDAP dialect. Spring Security provides ActiveDirectoryLdapAuthenticationProvider:
@Bean
public AuthenticationProvider activeDirectoryAuthProvider() {
ActiveDirectoryLdapAuthenticationProvider provider =
new ActiveDirectoryLdapAuthenticationProvider(
"example.com", // AD domain
"ldap://ad.example.com" // AD server URL
);
// AD-specific: use SAM account name for login
provider.setSearchFilter("(&(objectClass=user)(sAMAccountName={1}))");
// Convert AD groups to Spring roles
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
For Active Directory, the username can be:
alice→alice@example.com(UPN format, recommended)EXAMPLE\alice→ NT format
Embedded LDAP for Development
For local development and testing, use an embedded LDAP server:
# application-dev.yml
spring:
ldap:
embedded:
base-dn: dc=example,dc=com
ldif: classpath:test-ldap-data.ldif
port: 8389
validation:
enabled: false
# src/main/resources/test-ldap-data.ldif
dn: dc=example,dc=com
objectClass: top
objectClass: domain
dc: example
dn: ou=users,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: users
dn: ou=groups,dc=example,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups
# Users
dn: uid=alice,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
uid: alice
cn: Alice Smith
sn: Smith
mail: alice@example.com
userPassword: {bcrypt}$2a$10$hash...
dn: uid=bob,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
uid: bob
cn: Bob Jones
sn: Jones
userPassword: {bcrypt}$2a$10$hash...
# Groups
dn: cn=developers,ou=groups,dc=example,dc=com
objectClass: groupOfNames
cn: developers
member: uid=alice,ou=users,dc=example,dc=com
member: uid=bob,ou=users,dc=example,dc=com
dn: cn=admins,ou=groups,dc=example,dc=com
objectClass: groupOfNames
cn: admins
member: uid=alice,ou=users,dc=example,dc=com
// Profile-based configuration — embedded for dev, real LDAP for prod
@Configuration
@Profile("dev")
public class EmbeddedLdapConfig {
@Bean
public DefaultSpringSecurityContextSource contextSource() {
return new DefaultSpringSecurityContextSource(
"ldap://localhost:8389/dc=example,dc=com"
);
}
}
@Configuration
@Profile("prod")
public class ProductionLdapConfig {
@Value("${ldap.url}")
private String ldapUrl;
@Value("${ldap.manager-dn}")
private String managerDn;
@Value("${ldap.manager-password}")
private String managerPassword;
@Bean
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource ctx =
new DefaultSpringSecurityContextSource(ldapUrl);
ctx.setUserDn(managerDn);
ctx.setPassword(managerPassword);
return ctx;
}
}
Custom Group-to-Role Mapping
LDAP group names often don’t map cleanly to Spring Security roles. Implement a custom LdapAuthoritiesPopulator:
@Component
public class CustomLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
private static final Map<String, String> GROUP_TO_ROLE = Map.of(
"developers", "ROLE_DEVELOPER",
"devops", "ROLE_DEVOPS",
"qa-engineers", "ROLE_QA",
"team-leads", "ROLE_MANAGER",
"sre-team", "ROLE_SRE"
);
@Override
public Collection<? extends GrantedAuthority> getGrantedAuthorities(
DirContextOperations userData,
String username
) {
// Get group memberships from LDAP attribute
String[] groups = userData.getStringAttributes("memberOf");
if (groups == null) return List.of();
return Arrays.stream(groups)
.map(dn -> extractCn(dn).toLowerCase())
.map(cn -> GROUP_TO_ROLE.getOrDefault(cn, "ROLE_" + cn.toUpperCase()))
.map(SimpleGrantedAuthority::new)
.toList();
}
private String extractCn(String dn) {
// Extract CN from full DN: "CN=developers,OU=groups,DC=example,DC=com" → "developers"
return dn.split(",")[0].replace("CN=", "").replace("cn=", "");
}
}
Combining LDAP with Local Database Users
Many enterprise applications authenticate via LDAP but store additional user data (preferences, application-specific roles) in a local database. The hybrid pattern:
flowchart TD
Login[POST /login] --> AM[AuthenticationManager]
AM --> LDAP[LdapAuthenticationProvider\nVerify credentials]
LDAP -->|success| DB[Local UserRepository\nLoad profile + app roles]
DB -->|merge| Auth[Final Authentication\nLDAP groups + DB roles + profile]
Auth --> SC[SecurityContext]
@Component
public class HybridLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
private final UserRepository userRepository;
private final DefaultLdapAuthoritiesPopulator ldapPopulator;
@Override
public Collection<? extends GrantedAuthority> getGrantedAuthorities(
DirContextOperations userData,
String username
) {
// Get LDAP groups
Collection<GrantedAuthority> ldapAuthorities =
ldapPopulator.getGrantedAuthorities(userData, username);
// Merge with local DB roles
Set<GrantedAuthority> combined = new HashSet<>(ldapAuthorities);
userRepository.findByUsername(username)
.ifPresent(user -> user.getAppRoles()
.forEach(role -> combined.add(new SimpleGrantedAuthority("ROLE_" + role))));
return combined;
}
}
Logging and Debugging LDAP
logging:
level:
org.springframework.security.ldap: DEBUG
org.springframework.ldap: DEBUG
com.sun.jndi.ldap: ALL # very verbose — shows raw LDAP protocol messages
Common LDAP errors:
| Error | Cause | Fix |
|---|---|---|
CommunicationException | Cannot reach LDAP server | Check host/port, firewall |
AuthenticationException | Wrong manager DN/password | Verify service account credentials |
IncorrectResultSizeDataAccessException | Multiple users match search | Narrow search filter |
EmptyResultDataAccessException | User not found in LDAP | Check search base and filter |
InvalidNameException | Malformed DN | Verify DN format |
Summary
- LDAP authentication: Spring Security binds as a service account, searches for the user DN, then binds as the user to verify the password.
- Use
LdapAuthenticationProviderwithBindAuthenticatorandDefaultLdapAuthoritiesPopulatorfor standard LDAP. - Use
ActiveDirectoryLdapAuthenticationProviderfor Microsoft Active Directory. - Use embedded LDAP (UnboundID SDK) with an LDIF file for development and testing.
- Implement custom
LdapAuthoritiesPopulatorfor complex group-to-role mapping or merging LDAP groups with local DB roles.
Next: Article 11 covers X.509 certificate authentication — mutual TLS for service-to-service and enterprise client authentication.