Active Directory integration using Java provides secure authentication and directory services access. This guide covers multiple approaches for AD binding, from simple authentication to advanced directory operations.
Overview and Approaches
Authentication Methods:
- Simple Bind: Username/password authentication
- GSSAPI/Kerberos: Integrated Windows Authentication
- SSL/TLS: Secure encrypted connections
- Connection Pooling: Efficient resource management
Key Protocols:
- LDAP: Lightweight Directory Access Protocol
- LDAPS: LDAP over SSL/TLS
- SASL: Simple Authentication and Security Layer
Dependencies and Setup
Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<unboundid-ldap.version>6.0.8</unboundid-ldap.version>
<spring-ldap.version>3.1.0</spring-ldap.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- LDAP Libraries -->
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>${unboundid-ldap.version}</version>
</dependency>
<!-- Spring LDAP -->
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
<version>${spring-ldap.version}</version>
</dependency>
<!-- JNDI (Java Native) -->
<dependency>
<groupId>javax.naming</groupId>
<artifactId>jndi</artifactId>
<version>1.2.1</version>
</dependency>
<!-- Kerberos Support -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-kerberos-core</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
</dependencies>
Application Configuration
# application.yml
ad:
ldap:
url: "ldap://ad.company.com:389"
ssl-url: "ldaps://ad.company.com:636"
domain: "company.com"
base-dn: "DC=company,DC=com"
user-dn: "CN=Users,DC=company,DC=com"
group-dn: "CN=Groups,DC=company,DC=com"
search-base: "OU=Users,DC=company,DC=com"
bind-dn: "CN=ServiceAccount,CN=Users,DC=company,DC=com"
bind-password: "${AD_BIND_PASSWORD}"
connection-timeout: 5000
read-timeout: 60000
pool:
enabled: true
max-size: 20
min-size: 5
max-wait: 30000
spring:
ldap:
urls: "ldap://ad.company.com:389"
base: "DC=company,DC=com"
username: "CN=ServiceAccount,CN=Users,DC=company,DC=com"
password: "${AD_BIND_PASSWORD}"
Configuration Properties Class
@Configuration
@ConfigurationProperties(prefix = "ad.ldap")
@Data
public class ActiveDirectoryProperties {
private String url;
private String sslUrl;
private String domain;
private String baseDn;
private String userDn;
private String groupDn;
private String searchBase;
private String bindDn;
private String bindPassword;
private int connectionTimeout = 5000;
private int readTimeout = 60000;
private PoolProperties pool = new PoolProperties();
@Data
public static class PoolProperties {
private boolean enabled = true;
private int maxSize = 20;
private int minSize = 5;
private long maxWait = 30000;
}
public String getUserSearchBase() {
return userDn != null ? userDn : searchBase;
}
public String getGroupSearchBase() {
return groupDn != null ? groupDn : searchBase;
}
}
Core AD Binding Implementation
1. Active Directory Service
@Service
@Slf4j
public class ActiveDirectoryService {
private final ActiveDirectoryProperties adProperties;
private final LDAPConnectionPool connectionPool;
public ActiveDirectoryService(ActiveDirectoryProperties adProperties) {
this.adProperties = adProperties;
this.connectionPool = initializeConnectionPool();
}
private LDAPConnectionPool initializeConnectionPool() {
try {
LDAPConnectionOptions options = new LDAPConnectionOptions();
options.setConnectTimeoutMillis(adProperties.getConnectionTimeout());
options.setResponseTimeoutMillis(adProperties.getReadTimeout());
options.setAbandonOnTimeout(true);
ServerSet serverSet = new SingleServerSet(
adProperties.getUrl().replace("ldap://", "").replace("ldaps://", ""),
adProperties.getUrl().startsWith("ldaps://") ? 636 : 389
);
BindRequest bindRequest = new SimpleBindRequest(
adProperties.getBindDn(),
adProperties.getBindPassword()
);
return new LDAPConnectionPool(
serverSet,
bindRequest,
adProperties.getPool().getMaxSize()
);
} catch (Exception e) {
log.error("Failed to initialize AD connection pool", e);
throw new RuntimeException("AD connection pool initialization failed", e);
}
}
public boolean authenticateUser(String username, String password) {
LDAPConnection connection = null;
try {
connection = connectionPool.getConnection();
// Construct user DN
String userDn = constructUserDn(username);
// Attempt bind with user credentials
BindResult bindResult = connection.bind(userDn, password);
log.info("User authentication successful: {}", username);
return bindResult.getResultCode() == ResultCode.SUCCESS;
} catch (LDAPException e) {
log.warn("User authentication failed for {}: {}", username, e.getMessage());
return false;
} finally {
if (connection != null) {
connectionPool.releaseConnection(connection);
}
}
}
public UserDetails getUserDetails(String username) {
LDAPConnection connection = null;
try {
connection = connectionPool.getConnection();
String searchFilter = "(&(objectClass=user)(sAMAccountName=" + username + "))";
SearchRequest searchRequest = new SearchRequest(
adProperties.getUserSearchBase(),
SearchScope.SUB,
searchFilter,
"cn", "givenName", "sn", "mail", "sAMAccountName",
"userPrincipalName", "distinguishedName", "memberOf"
);
SearchResult searchResult = connection.search(searchRequest);
if (searchResult.getEntryCount() == 0) {
log.warn("User not found in AD: {}", username);
return null;
}
SearchResultEntry entry = searchResult.getSearchEntries().get(0);
return mapToUserDetails(entry);
} catch (LDAPException e) {
log.error("Failed to retrieve user details for {}: {}", username, e.getMessage());
return null;
} finally {
if (connection != null) {
connectionPool.releaseConnection(connection);
}
}
}
public List<String> getUserGroups(String username) {
LDAPConnection connection = null;
try {
connection = connectionPool.getConnection();
String searchFilter = "(&(objectClass=user)(sAMAccountName=" + username + "))";
SearchRequest searchRequest = new SearchRequest(
adProperties.getUserSearchBase(),
SearchScope.SUB,
searchFilter,
"memberOf"
);
SearchResult searchResult = connection.search(searchRequest);
if (searchResult.getEntryCount() == 0) {
return Collections.emptyList();
}
SearchResultEntry entry = searchResult.getSearchEntries().get(0);
Attribute memberOf = entry.getAttribute("memberOf");
if (memberOf == null) {
return Collections.emptyList();
}
return memberOf.getValues().stream()
.map(this::extractGroupName)
.collect(Collectors.toList());
} catch (LDAPException e) {
log.error("Failed to retrieve user groups for {}: {}", username, e.getMessage());
return Collections.emptyList();
} finally {
if (connection != null) {
connectionPool.releaseConnection(connection);
}
}
}
public boolean changeUserPassword(String username, String oldPassword, String newPassword) {
LDAPConnection connection = null;
try {
connection = connectionPool.getConnection();
String userDn = constructUserDn(username);
// First bind with old password to verify
connection.bind(userDn, oldPassword);
// Modify password
Modification mod = new Modification(
ModificationType.REPLACE,
"unicodePwd",
encodePassword(newPassword)
);
ModifyRequest modifyRequest = new ModifyRequest(userDn, mod);
ModifyResult modifyResult = connection.modify(modifyRequest);
boolean success = modifyResult.getResultCode() == ResultCode.SUCCESS;
if (success) {
log.info("Password changed successfully for user: {}", username);
} else {
log.warn("Password change failed for user {}: {}", username, modifyResult.getResultCode());
}
return success;
} catch (LDAPException e) {
log.error("Password change failed for user {}: {}", username, e.getMessage());
return false;
} finally {
if (connection != null) {
connectionPool.releaseConnection(connection);
}
}
}
public boolean isUserAccountLocked(String username) {
LDAPConnection connection = null;
try {
connection = connectionPool.getConnection();
String searchFilter = "(&(objectClass=user)(sAMAccountName=" + username + "))";
SearchRequest searchRequest = new SearchRequest(
adProperties.getUserSearchBase(),
SearchScope.SUB,
searchFilter,
"lockoutTime", "userAccountControl"
);
SearchResult searchResult = connection.search(searchRequest);
if (searchResult.getEntryCount() == 0) {
return false;
}
SearchResultEntry entry = searchResult.getSearchEntries().get(0);
// Check lockoutTime
Attribute lockoutTime = entry.getAttribute("lockoutTime");
if (lockoutTime != null && Long.parseLong(lockoutTime.getValue()) > 0) {
return true;
}
// Check userAccountControl for disabled account
Attribute userAccountControl = entry.getAttribute("userAccountControl");
if (userAccountControl != null) {
int uac = Integer.parseInt(userAccountControl.getValue());
// 0x0002 = ACCOUNTDISABLE
return (uac & 0x0002) != 0;
}
return false;
} catch (LDAPException e) {
log.error("Failed to check account lock status for {}: {}", username, e.getMessage());
return false;
} finally {
if (connection != null) {
connectionPool.releaseConnection(connection);
}
}
}
private String constructUserDn(String username) {
// Remove domain if present
if (username.contains("@")) {
username = username.substring(0, username.indexOf('@'));
}
if (username.contains("\\")) {
username = username.substring(username.indexOf('\\') + 1);
}
return String.format("CN=%s,%s", username, adProperties.getUserSearchBase());
}
private UserDetails mapToUserDetails(SearchResultEntry entry) {
String username = entry.getAttributeValue("sAMAccountName");
String email = entry.getAttributeValue("mail");
String firstName = entry.getAttributeValue("givenName");
String lastName = entry.getAttributeValue("sn");
String displayName = entry.getAttributeValue("cn");
List<String> groups = getUserGroups(username);
List<GrantedAuthority> authorities = groups.stream()
.map(group -> new SimpleGrantedAuthority("ROLE_" + group.toUpperCase()))
.collect(Collectors.toList());
return ActiveDirectoryUser.builder()
.username(username)
.email(email)
.firstName(firstName)
.lastName(lastName)
.displayName(displayName)
.dn(entry.getDN())
.authorities(authorities)
.accountNonExpired(true)
.accountNonLocked(!isUserAccountLocked(username))
.credentialsNonExpired(true)
.enabled(true)
.build();
}
private String extractGroupName(String groupDn) {
// Extract CN from DN (e.g., "CN=Administrators,CN=Groups,DC=company,DC=com" -> "Administrators")
if (groupDn.startsWith("CN=")) {
int endIndex = groupDn.indexOf(',');
if (endIndex > 0) {
return groupDn.substring(3, endIndex);
}
}
return groupDn;
}
private byte[] encodePassword(String password) {
// AD requires password to be surrounded by quotes and encoded in UTF-16LE
String quotedPassword = "\"" + password + "\"";
return quotedPassword.getBytes(StandardCharsets.UTF_16LE);
}
@PreDestroy
public void cleanup() {
if (connectionPool != null) {
connectionPool.close();
log.info("AD connection pool closed");
}
}
}
2. User Details Model
@Data
@Builder
@AllArgsConstructor
public class ActiveDirectoryUser implements UserDetails {
private String username;
private String email;
private String firstName;
private String lastName;
private String displayName;
private String dn;
private List<GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
@Override
public String getPassword() {
// Password is not stored for security reasons
return null;
}
public String getFullName() {
if (firstName != null && lastName != null) {
return firstName + " " + lastName;
}
return displayName != null ? displayName : username;
}
}
Spring Security Integration
1. Active Directory Authentication Provider
@Component
public class ActiveDirectoryAuthenticationProvider implements AuthenticationProvider {
private final ActiveDirectoryService adService;
public ActiveDirectoryAuthenticationProvider(ActiveDirectoryService adService) {
this.adService = adService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
try {
// Authenticate against AD
boolean authenticated = adService.authenticateUser(username, password);
if (!authenticated) {
throw new BadCredentialsException("Invalid credentials");
}
// Check if account is locked
if (adService.isUserAccountLocked(username)) {
throw new LockedException("User account is locked");
}
// Get user details
UserDetails userDetails = adService.getUserDetails(username);
if (userDetails == null) {
throw new UsernameNotFoundException("User not found in directory");
}
return new UsernamePasswordAuthenticationToken(
userDetails,
password,
userDetails.getAuthorities()
);
} catch (Exception e) {
throw new AuthenticationServiceException("AD authentication failed", e);
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
2. Security Configuration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final ActiveDirectoryAuthenticationProvider adAuthProvider;
public SecurityConfig(ActiveDirectoryAuthenticationProvider adAuthProvider) {
this.adAuthProvider = adAuthProvider;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**", "/health").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.rememberMe(remember -> remember
.key("ad-auth-key")
.tokenValiditySeconds(86400) // 24 hours
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(adAuthProvider);
}
}
JNDI-Based AD Integration (Alternative Approach)
1. JNDI Active Directory Service
@Service
@Slf4j
public class JndiActiveDirectoryService {
private final ActiveDirectoryProperties adProperties;
private final Hashtable<String, String> env;
public JndiActiveDirectoryService(ActiveDirectoryProperties adProperties) {
this.adProperties = adProperties;
this.env = createJndiEnvironment();
}
private Hashtable<String, String> createJndiEnvironment() {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, adProperties.getUrl());
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, adProperties.getBindDn());
env.put(Context.SECURITY_CREDENTIALS, adProperties.getBindPassword());
env.put(Context.REFERRAL, "follow");
// SSL configuration
if (adProperties.getUrl().startsWith("ldaps://")) {
env.put(Context.SECURITY_PROTOCOL, "ssl");
env.put("java.naming.ldap.factory.socket", "com.sun.jndi.ldap.LdapCtxFactory");
}
return env;
}
public boolean authenticateUser(String username, String password) {
DirContext context = null;
try {
Hashtable<String, String> userEnv = new Hashtable<>(env);
userEnv.put(Context.SECURITY_PRINCIPAL, constructUserPrincipal(username));
userEnv.put(Context.SECURITY_CREDENTIALS, password);
context = new InitialDirContext(userEnv);
log.info("JNDI authentication successful for user: {}", username);
return true;
} catch (AuthenticationException e) {
log.warn("JNDI authentication failed for {}: {}", username, e.getMessage());
return false;
} catch (NamingException e) {
log.error("JNDI authentication error for {}: {}", username, e.getMessage());
return false;
} finally {
closeContext(context);
}
}
public UserDetails searchUser(String username) {
DirContext context = null;
try {
context = new InitialDirContext(env);
String searchFilter = "(&(objectClass=user)(sAMAccountName=" + username + "))";
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(new String[]{
"cn", "givenName", "sn", "mail", "sAMAccountName",
"userPrincipalName", "distinguishedName", "memberOf"
});
NamingEnumeration<SearchResult> results = context.search(
adProperties.getUserSearchBase(), searchFilter, controls);
if (results.hasMore()) {
SearchResult result = results.next();
return mapSearchResultToUser(result);
}
return null;
} catch (NamingException e) {
log.error("JNDI search failed for {}: {}", username, e.getMessage());
return null;
} finally {
closeContext(context);
}
}
public List<String> getUserGroups(String username) {
DirContext context = null;
try {
context = new InitialDirContext(env);
String searchFilter = "(&(objectClass=user)(sAMAccountName=" + username + "))";
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(new String[]{"memberOf"});
NamingEnumeration<SearchResult> results = context.search(
adProperties.getUserSearchBase(), searchFilter, controls);
if (results.hasMore()) {
SearchResult result = results.next();
Attributes attributes = result.getAttributes();
Attribute memberOf = attributes.get("memberOf");
if (memberOf != null) {
List<String> groups = new ArrayList<>();
for (int i = 0; i < memberOf.size(); i++) {
groups.add(extractGroupName((String) memberOf.get(i)));
}
return groups;
}
}
return Collections.emptyList();
} catch (NamingException e) {
log.error("JNDI group search failed for {}: {}", username, e.getMessage());
return Collections.emptyList();
} finally {
closeContext(context);
}
}
private String constructUserPrincipal(String username) {
if (username.contains("@")) {
return username; // UPN format
} else {
return username + "@" + adProperties.getDomain();
}
}
private UserDetails mapSearchResultToUser(SearchResult result) throws NamingException {
Attributes attributes = result.getAttributes();
String username = getAttributeValue(attributes, "sAMAccountName");
String email = getAttributeValue(attributes, "mail");
String firstName = getAttributeValue(attributes, "givenName");
String lastName = getAttributeValue(attributes, "sn");
String displayName = getAttributeValue(attributes, "cn");
String dn = getAttributeValue(attributes, "distinguishedName");
List<String> groups = getUserGroups(username);
List<GrantedAuthority> authorities = groups.stream()
.map(group -> new SimpleGrantedAuthority("ROLE_" + group.toUpperCase()))
.collect(Collectors.toList());
return ActiveDirectoryUser.builder()
.username(username)
.email(email)
.firstName(firstName)
.lastName(lastName)
.displayName(displayName)
.dn(dn)
.authorities(authorities)
.accountNonExpired(true)
.accountNonLocked(true)
.credentialsNonExpired(true)
.enabled(true)
.build();
}
private String getAttributeValue(Attributes attributes, String attributeName) throws NamingException {
Attribute attribute = attributes.get(attributeName);
return attribute != null ? (String) attribute.get() : null;
}
private void closeContext(DirContext context) {
if (context != null) {
try {
context.close();
} catch (NamingException e) {
log.warn("Error closing JNDI context", e);
}
}
}
}
Advanced AD Operations
1. Group Management Service
@Service
@Slf4j
public class ActiveDirectoryGroupService {
private final ActiveDirectoryService adService;
private final ActiveDirectoryProperties adProperties;
public ActiveDirectoryGroupService(ActiveDirectoryService adService,
ActiveDirectoryProperties adProperties) {
this.adService = adService;
this.adProperties = adProperties;
}
public boolean addUserToGroup(String username, String groupName) {
LDAPConnection connection = null;
try {
connection = adService.getConnection();
String userDn = adService.constructUserDn(username);
String groupDn = findGroupDn(groupName);
if (groupDn == null) {
log.warn("Group not found: {}", groupName);
return false;
}
Modification mod = new Modification(
ModificationType.ADD,
"member",
userDn
);
ModifyRequest modifyRequest = new ModifyRequest(groupDn, mod);
ModifyResult result = connection.modify(modifyRequest);
boolean success = result.getResultCode() == ResultCode.SUCCESS;
if (success) {
log.info("User {} added to group {}", username, groupName);
}
return success;
} catch (LDAPException e) {
log.error("Failed to add user {} to group {}: {}", username, groupName, e.getMessage());
return false;
} finally {
if (connection != null) {
adService.releaseConnection(connection);
}
}
}
public boolean removeUserFromGroup(String username, String groupName) {
LDAPConnection connection = null;
try {
connection = adService.getConnection();
String userDn = adService.constructUserDn(username);
String groupDn = findGroupDn(groupName);
if (groupDn == null) {
log.warn("Group not found: {}", groupName);
return false;
}
Modification mod = new Modification(
ModificationType.DELETE,
"member",
userDn
);
ModifyRequest modifyRequest = new ModifyRequest(groupDn, mod);
ModifyResult result = connection.modify(modifyRequest);
boolean success = result.getResultCode() == ResultCode.SUCCESS;
if (success) {
log.info("User {} removed from group {}", username, groupName);
}
return success;
} catch (LDAPException e) {
log.error("Failed to remove user {} from group {}: {}", username, groupName, e.getMessage());
return false;
} finally {
if (connection != null) {
adService.releaseConnection(connection);
}
}
}
public List<String> getGroupMembers(String groupName) {
LDAPConnection connection = null;
try {
connection = adService.getConnection();
String groupDn = findGroupDn(groupName);
if (groupDn == null) {
return Collections.emptyList();
}
SearchRequest searchRequest = new SearchRequest(
groupDn,
SearchScope.BASE,
"(objectClass=group)",
"member"
);
SearchResult searchResult = connection.search(searchRequest);
if (searchResult.getEntryCount() == 0) {
return Collections.emptyList();
}
SearchResultEntry entry = searchResult.getSearchEntries().get(0);
Attribute member = entry.getAttribute("member");
if (member == null) {
return Collections.emptyList();
}
return member.getValues().stream()
.map(this::extractUsernameFromDn)
.collect(Collectors.toList());
} catch (LDAPException e) {
log.error("Failed to get group members for {}: {}", groupName, e.getMessage());
return Collections.emptyList();
} finally {
if (connection != null) {
adService.releaseConnection(connection);
}
}
}
public boolean createGroup(String groupName, String description) {
LDAPConnection connection = null;
try {
connection = adService.getConnection();
String groupDn = "CN=" + groupName + "," + adProperties.getGroupSearchBase();
ArrayList<Attribute> attributes = new ArrayList<>();
attributes.add(new Attribute("objectClass", "top", "group"));
attributes.add(new Attribute("cn", groupName));
attributes.add(new Attribute("sAMAccountName", groupName));
attributes.add(new Attribute("description", description));
attributes.add(new Attribute("groupType", "2147483650")); // Security group
AddRequest addRequest = new AddRequest(groupDn, attributes);
AddResult result = connection.add(addRequest);
boolean success = result.getResultCode() == ResultCode.SUCCESS;
if (success) {
log.info("Group created: {}", groupName);
}
return success;
} catch (LDAPException e) {
log.error("Failed to create group {}: {}", groupName, e.getMessage());
return false;
} finally {
if (connection != null) {
adService.releaseConnection(connection);
}
}
}
private String findGroupDn(String groupName) {
LDAPConnection connection = null;
try {
connection = adService.getConnection();
String searchFilter = "(&(objectClass=group)(cn=" + groupName + "))";
SearchRequest searchRequest = new SearchRequest(
adProperties.getGroupSearchBase(),
SearchScope.SUB,
searchFilter,
"distinguishedName"
);
SearchResult searchResult = connection.search(searchRequest);
if (searchResult.getEntryCount() > 0) {
return searchResult.getSearchEntries().get(0).getDN();
}
return null;
} catch (LDAPException e) {
log.error("Failed to find group DN for {}: {}", groupName, e.getMessage());
return null;
} finally {
if (connection != null) {
adService.releaseConnection(connection);
}
}
}
private String extractUsernameFromDn(String userDn) {
// Extract sAMAccountName from DN
if (userDn.startsWith("CN=")) {
int start = userDn.indexOf("CN=") + 3;
int end = userDn.indexOf(',', start);
if (end == -1) end = userDn.length();
return userDn.substring(start, end);
}
return userDn;
}
}
2. SSL/TLS Configuration
@Configuration
public class LdapSslConfig {
@Bean
public SSLUtil sslUtil() {
return new SSLUtil(new TrustAllTrustManager());
}
@Bean
public SocketFactory socketFactory() throws GeneralSecurityException {
return sslUtil().createSSLSocketFactory();
}
// Trust manager that accepts all certificates (use carefully!)
private static class TrustAllTrustManager extends X509ExtendedTrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
}
}
REST Controllers
1. Authentication Controller
@RestController
@RequestMapping("/api/auth")
@Validated
@Slf4j
public class AuthController {
private final ActiveDirectoryService adService;
private final JwtTokenService jwtTokenService;
public AuthController(ActiveDirectoryService adService, JwtTokenService jwtTokenService) {
this.adService = adService;
this.jwtTokenService = jwtTokenService;
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
try {
// Authenticate against AD
boolean authenticated = adService.authenticateUser(
request.getUsername(), request.getPassword());
if (!authenticated) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new AuthResponse(false, "Invalid credentials", null));
}
// Get user details
UserDetails userDetails = adService.getUserDetails(request.getUsername());
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new AuthResponse(false, "User not found", null));
}
// Generate JWT token
String token = jwtTokenService.generateToken(userDetails);
AuthResponse response = new AuthResponse(true, "Authentication successful", token);
response.setUser(userDetails);
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Login failed for user: {}", request.getUsername(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new AuthResponse(false, "Authentication service error", null));
}
}
@PostMapping("/validate")
public ResponseEntity<ValidationResponse> validateToken(@RequestHeader("Authorization") String authHeader) {
try {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String token = authHeader.substring(7);
boolean isValid = jwtTokenService.validateToken(token);
if (isValid) {
UserDetails userDetails = jwtTokenService.extractUserDetails(token);
return ResponseEntity.ok(new ValidationResponse(true, "Token valid", userDetails));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ValidationResponse(false, "Invalid token", null));
}
} catch (Exception e) {
log.error("Token validation failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ValidationResponse(false, "Validation error", null));
}
}
@PostMapping("/password/change")
public ResponseEntity<BaseResponse> changePassword(@Valid @RequestBody ChangePasswordRequest request,
@AuthenticationPrincipal UserDetails userDetails) {
try {
boolean success = adService.changeUserPassword(
userDetails.getUsername(),
request.getOldPassword(),
request.getNewPassword()
);
if (success) {
return ResponseEntity.ok(new BaseResponse(true, "Password changed successfully"));
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new BaseResponse(false, "Password change failed"));
}
} catch (Exception e) {
log.error("Password change failed for user: {}", userDetails.getUsername(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new BaseResponse(false, "Password change service error"));
}
}
}
@Data
class LoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
}
@Data
class ChangePasswordRequest {
@NotBlank
private String oldPassword;
@NotBlank
@Size(min = 8, message = "Password must be at least 8 characters long")
private String newPassword;
}
@Data
class AuthResponse {
private boolean success;
private String message;
private String token;
private UserDetails user;
public AuthResponse(boolean success, String message, String token) {
this.success = success;
this.message = message;
this.token = token;
}
}
@Data
class ValidationResponse {
private boolean valid;
private String message;
private UserDetails user;
public ValidationResponse(boolean valid, String message, UserDetails user) {
this.valid = valid;
this.message = message;
this.user = user;
}
}
@Data
class BaseResponse {
private boolean success;
private String message;
public BaseResponse(boolean success, String message) {
this.success = success;
this.message = message;
}
}
2. User Management Controller
@RestController
@RequestMapping("/api/users")
@PreAuthorize("hasRole('ADMIN')")
@Slf4j
public class UserManagementController {
private final ActiveDirectoryService adService;
private final ActiveDirectoryGroupService groupService;
public UserManagementController(ActiveDirectoryService adService,
ActiveDirectoryGroupService groupService) {
this.adService = adService;
this.groupService = groupService;
}
@GetMapping("/{username}")
public ResponseEntity<UserDetails> getUser(@PathVariable String username) {
try {
UserDetails userDetails = adService.getUserDetails(username);
if (userDetails == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(userDetails);
} catch (Exception e) {
log.error("Failed to get user details: {}", username, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/{username}/groups")
public ResponseEntity<List<String>> getUserGroups(@PathVariable String username) {
try {
List<String> groups = adService.getUserGroups(username);
return ResponseEntity.ok(groups);
} catch (Exception e) {
log.error("Failed to get user groups: {}", username, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/{username}/groups/{groupName}")
public ResponseEntity<BaseResponse> addUserToGroup(@PathVariable String username,
@PathVariable String groupName) {
try {
boolean success = groupService.addUserToGroup(username, groupName);
if (success) {
return ResponseEntity.ok(new BaseResponse(true, "User added to group"));
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new BaseResponse(false, "Failed to add user to group"));
}
} catch (Exception e) {
log.error("Failed to add user to group: {} -> {}", username, groupName, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new BaseResponse(false, "Service error"));
}
}
@GetMapping("/{username}/account-status")
public ResponseEntity<AccountStatusResponse> getAccountStatus(@PathVariable String username) {
try {
boolean isLocked = adService.isUserAccountLocked(username);
UserDetails userDetails = adService.getUserDetails(username);
AccountStatusResponse response = new AccountStatusResponse();
response.setUsername(username);
response.setLocked(isLocked);
response.setEnabled(userDetails != null && userDetails.isEnabled());
response.setExists(userDetails != null);
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to get account status: {}", username, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
@Data
class AccountStatusResponse {
private String username;
private boolean exists;
private boolean locked;
private boolean enabled;
}
Testing
1. Unit Tests
@ExtendWith(MockitoExtension.class)
class ActiveDirectoryServiceTest {
@Mock
private LDAPConnectionPool connectionPool;
@Mock
private LDAPConnection connection;
@InjectMocks
private ActiveDirectoryService adService;
@Test
void testSuccessfulAuthentication() throws Exception {
when(connectionPool.getConnection()).thenReturn(connection);
when(connection.bind(anyString(), anyString())).thenReturn(new BindResult(1, ResultCode.SUCCESS, null, null, null, null));
boolean result = adService.authenticateUser("testuser", "password");
assertTrue(result);
verify(connectionPool).releaseConnection(connection);
}
@Test
void testFailedAuthentication() throws Exception {
when(connectionPool.getConnection()).thenReturn(connection);
when(connection.bind(anyString(), anyString())).thenThrow(new LDAPException(ResultCode.INVALID_CREDENTIALS));
boolean result = adService.authenticateUser("testuser", "wrongpassword");
assertFalse(result);
verify(connectionPool).releaseConnection(connection);
}
}
@SpringBootTest
class ActiveDirectoryIntegrationTest {
@Autowired
private ActiveDirectoryService adService;
@Test
void testUserAuthentication() {
// This would test against a real AD instance in integration environment
boolean result = adService.authenticateUser("testuser", "password");
// Assert based on expected results
assertTrue(result);
}
}
2. Test Configuration
@TestConfiguration
public class TestAdConfig {
@Bean
@Primary
public ActiveDirectoryProperties testAdProperties() {
ActiveDirectoryProperties properties = new ActiveDirectoryProperties();
properties.setUrl("ldap://test-ad.company.com:389");
properties.setDomain("company.com");
properties.setBaseDn("DC=company,DC=com");
properties.setUserDn("CN=Users,DC=company,DC=com");
properties.setBindDn("CN=TestUser,CN=Users,DC=company,DC=com");
properties.setBindPassword("testpassword");
return properties;
}
}
Best Practices
- Security:
- Use LDAPS (SSL/TLS) in production
- Implement proper certificate validation
- Store service account credentials securely
- Use connection pooling with appropriate limits
- Error Handling:
- Handle LDAP exceptions gracefully
- Provide meaningful error messages
- Implement retry mechanisms for transient failures
- Performance:
- Use connection pooling
- Implement query timeouts
- Cache frequently accessed data
- Monitoring:
- Log authentication attempts
- Monitor connection pool usage
- Track failed authentication attempts
// Example of secure credential handling
@Component
public class SecureCredentialManager {
public String decryptPassword(String encryptedPassword) {
// Implement secure password decryption
// Use AWS KMS, HashiCorp Vault, or similar services
return new String(Base64.getDecoder().decode(encryptedPassword));
}
}
Troubleshooting Common Issues
@Component
@Slf4j
public class AdConnectionTroubleshooter {
public void diagnoseConnection(ActiveDirectoryProperties properties) {
try {
// Test basic connectivity
LDAPConnection connection = new LDAPConnection();
connection.connect(properties.getUrl().replace("ldap://", ""), 389);
// Test bind
connection.bind(properties.getBindDn(), properties.getBindPassword());
log.info("AD connection test successful");
connection.close();
} catch (LDAPException e) {
log.error("AD connection test failed: {}", e.getMessage());
switch (e.getResultCode()) {
case INVALID_CREDENTIALS:
log.error("Invalid bind credentials");
break;
case CONNECT_ERROR:
log.error("Cannot connect to AD server");
break;
case UNWILLING_TO_PERFORM:
log.error("Server unwilling to perform operation");
break;
default:
log.error("Unexpected error: {}", e.getMessage());
}
}
}
}
Conclusion
Active Directory binding in Java provides:
- Secure authentication against enterprise directories
- Comprehensive user management capabilities
- Group-based authorization and role management
- Production-ready with connection pooling and error handling
The implementation supports multiple authentication methods, secure connections, and integrates seamlessly with Spring Security for enterprise-grade applications.