Spring Security LDAP module providing comprehensive LDAP authentication and authorization capabilities for enterprise applications
—
LDAP password policy controls and exception handling for enterprise environments requiring advanced password management features.
LDAP password policy request control for requesting password policy information during authentication.
/**
* LDAP password policy request control for requesting password policy information
* during bind operations and other LDAP operations
*/
public class PasswordPolicyControl extends BasicControl {
/**
* The object identifier for the password policy control
*/
public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1";
/**
* Creates a password policy control with default criticality (false)
*/
public PasswordPolicyControl();
/**
* Creates a password policy control with specified criticality
* @param criticality true if the control is critical to the operation
*/
public PasswordPolicyControl(boolean criticality);
/**
* Gets the control's object identifier
* @return the OID string
*/
public String getID();
/**
* Indicates whether this control is critical
* @return true if critical
*/
public boolean isCritical();
/**
* Gets the encoded value of the control (always null for request control)
* @return null as this is a request control
*/
public byte[] getEncodedValue();
}Usage Examples:
// Add password policy control to LDAP context
LdapContext ctx = (LdapContext) contextSource.getContext("uid=user,ou=people", "password");
Control[] requestControls = { new PasswordPolicyControl() };
ctx.setRequestControls(requestControls);
// Perform authentication with password policy information
NamingEnumeration<SearchResult> results = ctx.search("ou=people", "uid=user", null);
// Check response controls for password policy information
Control[] responseControls = ctx.getResponseControls();
for (Control control : responseControls) {
if (control instanceof PasswordPolicyResponseControl) {
PasswordPolicyResponseControl ppControl = (PasswordPolicyResponseControl) control;
// Process password policy response
}
}Exception thrown when password policy violations occur during LDAP authentication.
/**
* Exception thrown when LDAP password policy violations are detected
* Extends BadCredentialsException to integrate with Spring Security exception handling
*/
public class PasswordPolicyException extends BadCredentialsException {
/**
* Creates a password policy exception with the specified error status
* @param status the password policy error status
*/
public PasswordPolicyException(PasswordPolicyErrorStatus status);
/**
* Creates a password policy exception with status and custom message
* @param status the password policy error status
* @param msg custom error message
*/
public PasswordPolicyException(PasswordPolicyErrorStatus status, String msg);
/**
* Creates a password policy exception with status, message, and cause
* @param status the password policy error status
* @param msg custom error message
* @param cause the underlying cause exception
*/
public PasswordPolicyException(PasswordPolicyErrorStatus status, String msg, Throwable cause);
/**
* Gets the password policy error status
* @return the error status that caused this exception
*/
public PasswordPolicyErrorStatus getStatus();
}Enumeration of standard LDAP password policy error status codes.
/**
* Enumeration of LDAP password policy error status codes as defined in
* the LDAP Password Policy specification
*/
public enum PasswordPolicyErrorStatus {
/**
* The user's password has expired and must be changed
*/
PASSWORD_EXPIRED(0),
/**
* The user's account is locked due to too many failed authentication attempts
*/
ACCOUNT_LOCKED(1),
/**
* The user must change their password after reset by administrator
*/
CHANGE_AFTER_RESET(2),
/**
* Password modifications are not allowed for this user
*/
PASSWORD_MOD_NOT_ALLOWED(3),
/**
* The user must supply their old password when changing password
*/
MUST_SUPPLY_OLD_PASSWORD(4),
/**
* The new password does not meet quality requirements
*/
INSUFFICIENT_PASSWORD_QUALITY(5),
/**
* The new password is too short according to policy
*/
PASSWORD_TOO_SHORT(6),
/**
* The password was changed too recently and cannot be changed again yet
*/
PASSWORD_TOO_YOUNG(7),
/**
* The new password matches a password in the user's password history
*/
PASSWORD_IN_HISTORY(8);
private final int value;
/**
* Creates an error status with the specified numeric value
* @param value the numeric error code
*/
PasswordPolicyErrorStatus(int value);
/**
* Gets the numeric value of this error status
* @return the numeric error code
*/
public int getValue();
/**
* Gets the error status for a numeric value
* @param value the numeric error code
* @return the corresponding error status, or null if not found
*/
public static PasswordPolicyErrorStatus valueOf(int value);
}Enhanced context source that automatically handles password policy controls.
/**
* Enhanced LDAP context source that automatically includes password policy controls
* in LDAP operations and processes policy responses
*/
public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityContextSource {
/**
* Creates a password policy aware context source
* @param providerUrl the LDAP provider URL
*/
public PasswordPolicyAwareContextSource(String providerUrl);
/**
* Gets an authenticated context with password policy control included
* @param principal the authentication principal
* @param credentials the authentication credentials
* @return DirContext with password policy awareness
* @throws NamingException if context creation fails
* @throws PasswordPolicyException if password policy violations occur
*/
@Override
public DirContext getContext(String principal, String credentials) throws NamingException;
/**
* Sets whether to throw exceptions on password policy warnings
* @param throwOnWarning true to throw exceptions on warnings
*/
public void setThrowExceptionOnPolicyWarning(boolean throwOnWarning);
}Response control containing password policy information from LDAP operations.
/**
* LDAP password policy response control containing policy information returned by the server
*/
public class PasswordPolicyResponseControl extends PasswordPolicyControl {
/**
* Creates a password policy response control from encoded bytes
* @param encoded the encoded control value
* @throws IOException if decoding fails
*/
public PasswordPolicyResponseControl(byte[] encoded) throws IOException;
/**
* Gets the time in seconds before password expiration
* @return seconds until expiration, or -1 if not specified
*/
public int getTimeBeforeExpiration();
/**
* Gets the number of grace authentications remaining
* @return number of grace logins, or -1 if not specified
*/
public int getGraceLoginsRemaining();
/**
* Gets the password policy error status
* @return error status, or null if no error
*/
public PasswordPolicyErrorStatus getErrorStatus();
/**
* Indicates whether an error status is present
* @return true if error status exists
*/
public boolean hasError();
/**
* Indicates whether warning information is present
* @return true if warning information exists
*/
public boolean hasWarning();
}Interface for objects that can carry password policy information.
/**
* Interface implemented by UserDetails objects that can carry password policy information
*/
public interface PasswordPolicyData {
/**
* Gets the time in seconds before password expiration
* @return seconds until expiration
*/
int getTimeBeforeExpiration();
/**
* Gets the number of grace authentications remaining
* @return number of grace logins
*/
int getGraceLoginsRemaining();
/**
* Sets the time before password expiration
* @param timeBeforeExpiration seconds until expiration
*/
void setTimeBeforeExpiration(int timeBeforeExpiration);
/**
* Sets the number of grace authentications remaining
* @param graceLoginsRemaining number of grace logins
*/
void setGraceLoginsRemaining(int graceLoginsRemaining);
}Factory for creating password policy controls from LDAP control responses.
/**
* Factory class for creating password policy controls from LDAP responses
*/
public class PasswordPolicyControlFactory extends ControlFactory {
/**
* Creates a control from the provided control information
* @param ctl the control to process
* @return PasswordPolicyResponseControl if applicable, otherwise the original control
* @throws NamingException if control creation fails
*/
@Override
public Control getControlInstance(Control ctl) throws NamingException;
}Utility class for extracting password policy information from LDAP response controls.
/**
* Utility class for extracting password policy response information from LDAP controls
*/
public final class PasswordPolicyControlExtractor {
/**
* Extracts password policy response information from an LDAP response control
* @param control the password policy response control
* @return PasswordPolicyResponse containing policy information
* @throws PasswordPolicyException if policy violations are detected
*/
public static PasswordPolicyResponse extractPasswordPolicyResponse(PasswordPolicyResponseControl control)
throws PasswordPolicyException;
/**
* Checks LDAP response controls for password policy information
* @param responseControls array of LDAP response controls
* @return PasswordPolicyResponse if policy control found, null otherwise
* @throws PasswordPolicyException if policy violations are detected
*/
public static PasswordPolicyResponse checkForPasswordPolicyControl(Control[] responseControls)
throws PasswordPolicyException;
}/**
* Enhanced LDAP authenticator that processes password policy controls
*/
public class PolicyAwareBindAuthenticator extends BindAuthenticator {
/**
* Creates a policy-aware bind authenticator
* @param contextSource the LDAP context source
*/
public PolicyAwareBindAuthenticator(ContextSource contextSource);
/**
* Authenticates with password policy control processing
* @param authentication the authentication request
* @return DirContextOperations with policy information
* @throws PasswordPolicyException if policy violations occur
*/
@Override
public DirContextOperations authenticate(Authentication authentication) {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
try {
// Get user DN
String userDn = getUserDn(username);
// Create context with password policy control
LdapContext ctx = (LdapContext) getContextSource().getContext(userDn, password);
Control[] requestControls = { new PasswordPolicyControl() };
ctx.setRequestControls(requestControls);
// Perform a simple operation to trigger policy evaluation
ctx.getAttributes("", new String[]{"1.1"});
// Check for password policy response
Control[] responseControls = ctx.getResponseControls();
if (responseControls != null) {
PasswordPolicyResponse response =
PasswordPolicyControlExtractor.checkForPasswordPolicyControl(responseControls);
if (response != null) {
processPasswordPolicyResponse(response, username);
}
}
// Return user context information
return createUserContext(ctx, username);
} catch (NamingException e) {
throw new BadCredentialsException("Authentication failed", e);
}
}
private void processPasswordPolicyResponse(PasswordPolicyResponse response, String username) {
// Handle password policy warnings
if (response.getTimeBeforeExpiration() > 0) {
logger.info("Password expires in {} seconds for user: {}",
response.getTimeBeforeExpiration(), username);
}
if (response.getGraceAuthNsRemaining() > 0) {
logger.warn("User {} has {} grace logins remaining",
username, response.getGraceAuthNsRemaining());
}
// Handle password policy errors
if (response.hasError()) {
PasswordPolicyErrorStatus error = response.getErrorStatus();
throw new PasswordPolicyException(error,
"Password policy violation: " + error.name());
}
}
}/**
* Container for password policy response information
*/
public class PasswordPolicyResponse {
private final int timeBeforeExpiration;
private final int graceAuthNsRemaining;
private final PasswordPolicyErrorStatus errorStatus;
/**
* Creates a password policy response
* @param timeBeforeExpiration seconds until password expires (0 if not applicable)
* @param graceAuthNsRemaining number of grace authentications remaining
* @param errorStatus error status, or null if no error
*/
public PasswordPolicyResponse(int timeBeforeExpiration, int graceAuthNsRemaining,
PasswordPolicyErrorStatus errorStatus);
/**
* Gets the time in seconds before password expiration
* @return seconds until expiration, or 0 if not applicable
*/
public int getTimeBeforeExpiration();
/**
* Gets the number of grace authentications remaining
* @return number of grace logins, or 0 if not applicable
*/
public int getGraceAuthNsRemaining();
/**
* Gets the password policy error status
* @return error status, or null if no error
*/
public PasswordPolicyErrorStatus getErrorStatus();
/**
* Indicates whether this response contains an error
* @return true if error status is present
*/
public boolean hasError();
/**
* Indicates whether this response contains warnings
* @return true if expiration warning or grace login information present
*/
public boolean hasWarning();
}@Configuration
public class PasswordPolicyConfig {
@Bean
public PolicyAwareBindAuthenticator policyAwareAuthenticator() {
PolicyAwareBindAuthenticator authenticator =
new PolicyAwareBindAuthenticator(contextSource());
authenticator.setUserSearch(userSearch());
return authenticator;
}
@Bean
public LdapAuthenticationProvider policyAwareAuthProvider() {
LdapAuthenticationProvider provider =
new LdapAuthenticationProvider(policyAwareAuthenticator());
// Set custom authentication exception handler
provider.setAuthenticationExceptionHandler(passwordPolicyExceptionHandler());
return provider;
}
@Bean
public AuthenticationExceptionHandler passwordPolicyExceptionHandler() {
return new PasswordPolicyAuthenticationExceptionHandler();
}
}@Component
public class PasswordPolicyAuthenticationExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(
PasswordPolicyAuthenticationExceptionHandler.class);
public void handlePasswordPolicyException(PasswordPolicyException ex, String username) {
PasswordPolicyErrorStatus status = ex.getStatus();
switch (status) {
case PASSWORD_EXPIRED:
logger.warn("Password expired for user: {}", username);
// Redirect to password change page
break;
case ACCOUNT_LOCKED:
logger.warn("Account locked for user: {}", username);
// Send account locked notification
break;
case CHANGE_AFTER_RESET:
logger.info("User {} must change password after reset", username);
// Force password change workflow
break;
case INSUFFICIENT_PASSWORD_QUALITY:
logger.info("Password quality insufficient for user: {}", username);
// Show password requirements
break;
case PASSWORD_TOO_SHORT:
logger.info("Password too short for user: {}", username);
// Show minimum length requirement
break;
default:
logger.error("Password policy violation for user {}: {}", username, status);
break;
}
}
}@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public AuthenticationFailureHandler ldapAuthenticationFailureHandler() {
return new LdapPasswordPolicyAuthenticationFailureHandler();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/password-change").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.failureHandler(ldapAuthenticationFailureHandler())
.permitAll()
)
.logout(logout -> logout.permitAll());
return http.build();
}
}
@Component
public class LdapPasswordPolicyAuthenticationFailureHandler
implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof PasswordPolicyException) {
PasswordPolicyException ppEx = (PasswordPolicyException) exception;
PasswordPolicyErrorStatus status = ppEx.getStatus();
switch (status) {
case PASSWORD_EXPIRED:
case CHANGE_AFTER_RESET:
response.sendRedirect("/password-change?expired=true");
return;
case ACCOUNT_LOCKED:
response.sendRedirect("/login?locked=true");
return;
default:
break;
}
}
// Default failure handling
response.sendRedirect("/login?error=true");
}
}@Controller
public class PasswordChangeController {
private final LdapTemplate ldapTemplate;
private final PasswordEncoder passwordEncoder;
public PasswordChangeController(LdapTemplate ldapTemplate, PasswordEncoder passwordEncoder) {
this.ldapTemplate = ldapTemplate;
this.passwordEncoder = passwordEncoder;
}
@GetMapping("/password-change")
public String showPasswordChangeForm(Model model,
@RequestParam(required = false) String expired) {
if ("true".equals(expired)) {
model.addAttribute("message", "Your password has expired and must be changed.");
}
return "password-change";
}
@PostMapping("/password-change")
public String changePassword(@RequestParam String currentPassword,
@RequestParam String newPassword,
@RequestParam String confirmPassword,
Authentication authentication,
RedirectAttributes redirectAttributes) {
try {
if (!newPassword.equals(confirmPassword)) {
redirectAttributes.addFlashAttribute("error", "Passwords do not match");
return "redirect:/password-change";
}
String username = authentication.getName();
String userDn = findUserDn(username);
// Validate current password
validateCurrentPassword(userDn, currentPassword);
// Change password with policy compliance check
changeUserPassword(userDn, newPassword);
redirectAttributes.addFlashAttribute("success", "Password changed successfully");
return "redirect:/dashboard";
} catch (PasswordPolicyException ex) {
String errorMessage = getPasswordPolicyErrorMessage(ex.getStatus());
redirectAttributes.addFlashAttribute("error", errorMessage);
return "redirect:/password-change";
} catch (Exception ex) {
redirectAttributes.addFlashAttribute("error", "Password change failed");
return "redirect:/password-change";
}
}
private void changeUserPassword(String userDn, String newPassword) {
// Use LDAP modify operation with password policy control
LdapContext ctx = (LdapContext) ldapTemplate.getContextSource().getContext(
"cn=admin,dc=example,dc=com", "adminPassword");
try {
Control[] requestControls = { new PasswordPolicyControl() };
ctx.setRequestControls(requestControls);
ModificationItem[] mods = new ModificationItem[] {
new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("userPassword", passwordEncoder.encode(newPassword)))
};
ctx.modifyAttributes(userDn, mods);
// Check response for policy violations
Control[] responseControls = ctx.getResponseControls();
PasswordPolicyControlExtractor.checkForPasswordPolicyControl(responseControls);
} catch (NamingException ex) {
throw new RuntimeException("Failed to change password", ex);
} finally {
LdapUtils.closeContext(ctx);
}
}
private String getPasswordPolicyErrorMessage(PasswordPolicyErrorStatus status) {
switch (status) {
case INSUFFICIENT_PASSWORD_QUALITY:
return "Password does not meet quality requirements";
case PASSWORD_TOO_SHORT:
return "Password is too short";
case PASSWORD_IN_HISTORY:
return "Password was used recently and cannot be reused";
case PASSWORD_TOO_YOUNG:
return "Password was changed too recently";
default:
return "Password policy violation: " + status.name();
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-security--spring-security-ldap