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.