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:

  • alicealice@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:

ErrorCauseFix
CommunicationExceptionCannot reach LDAP serverCheck host/port, firewall
AuthenticationExceptionWrong manager DN/passwordVerify service account credentials
IncorrectResultSizeDataAccessExceptionMultiple users match searchNarrow search filter
EmptyResultDataAccessExceptionUser not found in LDAPCheck search base and filter
InvalidNameExceptionMalformed DNVerify 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 LdapAuthenticationProvider with BindAuthenticator and DefaultLdapAuthoritiesPopulator for standard LDAP.
  • Use ActiveDirectoryLdapAuthenticationProvider for Microsoft Active Directory.
  • Use embedded LDAP (UnboundID SDK) with an LDIF file for development and testing.
  • Implement custom LdapAuthoritiesPopulator for 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.