0
# Password Policy Support
1
2
LDAP password policy controls and exception handling for enterprise environments requiring advanced password management features.
3
4
## Capabilities
5
6
### PasswordPolicyControl
7
8
LDAP password policy request control for requesting password policy information during authentication.
9
10
```java { .api }
11
/**
12
* LDAP password policy request control for requesting password policy information
13
* during bind operations and other LDAP operations
14
*/
15
public class PasswordPolicyControl extends BasicControl {
16
/**
17
* The object identifier for the password policy control
18
*/
19
public static final String OID = "1.3.6.1.4.1.42.2.27.8.5.1";
20
21
/**
22
* Creates a password policy control with default criticality (false)
23
*/
24
public PasswordPolicyControl();
25
26
/**
27
* Creates a password policy control with specified criticality
28
* @param criticality true if the control is critical to the operation
29
*/
30
public PasswordPolicyControl(boolean criticality);
31
32
/**
33
* Gets the control's object identifier
34
* @return the OID string
35
*/
36
public String getID();
37
38
/**
39
* Indicates whether this control is critical
40
* @return true if critical
41
*/
42
public boolean isCritical();
43
44
/**
45
* Gets the encoded value of the control (always null for request control)
46
* @return null as this is a request control
47
*/
48
public byte[] getEncodedValue();
49
}
50
```
51
52
**Usage Examples:**
53
54
```java
55
// Add password policy control to LDAP context
56
LdapContext ctx = (LdapContext) contextSource.getContext("uid=user,ou=people", "password");
57
Control[] requestControls = { new PasswordPolicyControl() };
58
ctx.setRequestControls(requestControls);
59
60
// Perform authentication with password policy information
61
NamingEnumeration<SearchResult> results = ctx.search("ou=people", "uid=user", null);
62
63
// Check response controls for password policy information
64
Control[] responseControls = ctx.getResponseControls();
65
for (Control control : responseControls) {
66
if (control instanceof PasswordPolicyResponseControl) {
67
PasswordPolicyResponseControl ppControl = (PasswordPolicyResponseControl) control;
68
// Process password policy response
69
}
70
}
71
```
72
73
### PasswordPolicyException
74
75
Exception thrown when password policy violations occur during LDAP authentication.
76
77
```java { .api }
78
/**
79
* Exception thrown when LDAP password policy violations are detected
80
* Extends BadCredentialsException to integrate with Spring Security exception handling
81
*/
82
public class PasswordPolicyException extends BadCredentialsException {
83
/**
84
* Creates a password policy exception with the specified error status
85
* @param status the password policy error status
86
*/
87
public PasswordPolicyException(PasswordPolicyErrorStatus status);
88
89
/**
90
* Creates a password policy exception with status and custom message
91
* @param status the password policy error status
92
* @param msg custom error message
93
*/
94
public PasswordPolicyException(PasswordPolicyErrorStatus status, String msg);
95
96
/**
97
* Creates a password policy exception with status, message, and cause
98
* @param status the password policy error status
99
* @param msg custom error message
100
* @param cause the underlying cause exception
101
*/
102
public PasswordPolicyException(PasswordPolicyErrorStatus status, String msg, Throwable cause);
103
104
/**
105
* Gets the password policy error status
106
* @return the error status that caused this exception
107
*/
108
public PasswordPolicyErrorStatus getStatus();
109
}
110
```
111
112
### PasswordPolicyErrorStatus
113
114
Enumeration of standard LDAP password policy error status codes.
115
116
```java { .api }
117
/**
118
* Enumeration of LDAP password policy error status codes as defined in
119
* the LDAP Password Policy specification
120
*/
121
public enum PasswordPolicyErrorStatus {
122
/**
123
* The user's password has expired and must be changed
124
*/
125
PASSWORD_EXPIRED(0),
126
127
/**
128
* The user's account is locked due to too many failed authentication attempts
129
*/
130
ACCOUNT_LOCKED(1),
131
132
/**
133
* The user must change their password after reset by administrator
134
*/
135
CHANGE_AFTER_RESET(2),
136
137
/**
138
* Password modifications are not allowed for this user
139
*/
140
PASSWORD_MOD_NOT_ALLOWED(3),
141
142
/**
143
* The user must supply their old password when changing password
144
*/
145
MUST_SUPPLY_OLD_PASSWORD(4),
146
147
/**
148
* The new password does not meet quality requirements
149
*/
150
INSUFFICIENT_PASSWORD_QUALITY(5),
151
152
/**
153
* The new password is too short according to policy
154
*/
155
PASSWORD_TOO_SHORT(6),
156
157
/**
158
* The password was changed too recently and cannot be changed again yet
159
*/
160
PASSWORD_TOO_YOUNG(7),
161
162
/**
163
* The new password matches a password in the user's password history
164
*/
165
PASSWORD_IN_HISTORY(8);
166
167
private final int value;
168
169
/**
170
* Creates an error status with the specified numeric value
171
* @param value the numeric error code
172
*/
173
PasswordPolicyErrorStatus(int value);
174
175
/**
176
* Gets the numeric value of this error status
177
* @return the numeric error code
178
*/
179
public int getValue();
180
181
/**
182
* Gets the error status for a numeric value
183
* @param value the numeric error code
184
* @return the corresponding error status, or null if not found
185
*/
186
public static PasswordPolicyErrorStatus valueOf(int value);
187
}
188
```
189
190
### PasswordPolicyAwareContextSource
191
192
Enhanced context source that automatically handles password policy controls.
193
194
```java { .api }
195
/**
196
* Enhanced LDAP context source that automatically includes password policy controls
197
* in LDAP operations and processes policy responses
198
*/
199
public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityContextSource {
200
/**
201
* Creates a password policy aware context source
202
* @param providerUrl the LDAP provider URL
203
*/
204
public PasswordPolicyAwareContextSource(String providerUrl);
205
206
/**
207
* Gets an authenticated context with password policy control included
208
* @param principal the authentication principal
209
* @param credentials the authentication credentials
210
* @return DirContext with password policy awareness
211
* @throws NamingException if context creation fails
212
* @throws PasswordPolicyException if password policy violations occur
213
*/
214
@Override
215
public DirContext getContext(String principal, String credentials) throws NamingException;
216
217
/**
218
* Sets whether to throw exceptions on password policy warnings
219
* @param throwOnWarning true to throw exceptions on warnings
220
*/
221
public void setThrowExceptionOnPolicyWarning(boolean throwOnWarning);
222
}
223
```
224
225
### PasswordPolicyResponseControl
226
227
Response control containing password policy information from LDAP operations.
228
229
```java { .api }
230
/**
231
* LDAP password policy response control containing policy information returned by the server
232
*/
233
public class PasswordPolicyResponseControl extends PasswordPolicyControl {
234
/**
235
* Creates a password policy response control from encoded bytes
236
* @param encoded the encoded control value
237
* @throws IOException if decoding fails
238
*/
239
public PasswordPolicyResponseControl(byte[] encoded) throws IOException;
240
241
/**
242
* Gets the time in seconds before password expiration
243
* @return seconds until expiration, or -1 if not specified
244
*/
245
public int getTimeBeforeExpiration();
246
247
/**
248
* Gets the number of grace authentications remaining
249
* @return number of grace logins, or -1 if not specified
250
*/
251
public int getGraceLoginsRemaining();
252
253
/**
254
* Gets the password policy error status
255
* @return error status, or null if no error
256
*/
257
public PasswordPolicyErrorStatus getErrorStatus();
258
259
/**
260
* Indicates whether an error status is present
261
* @return true if error status exists
262
*/
263
public boolean hasError();
264
265
/**
266
* Indicates whether warning information is present
267
* @return true if warning information exists
268
*/
269
public boolean hasWarning();
270
}
271
```
272
273
### PasswordPolicyData
274
275
Interface for objects that can carry password policy information.
276
277
```java { .api }
278
/**
279
* Interface implemented by UserDetails objects that can carry password policy information
280
*/
281
public interface PasswordPolicyData {
282
/**
283
* Gets the time in seconds before password expiration
284
* @return seconds until expiration
285
*/
286
int getTimeBeforeExpiration();
287
288
/**
289
* Gets the number of grace authentications remaining
290
* @return number of grace logins
291
*/
292
int getGraceLoginsRemaining();
293
294
/**
295
* Sets the time before password expiration
296
* @param timeBeforeExpiration seconds until expiration
297
*/
298
void setTimeBeforeExpiration(int timeBeforeExpiration);
299
300
/**
301
* Sets the number of grace authentications remaining
302
* @param graceLoginsRemaining number of grace logins
303
*/
304
void setGraceLoginsRemaining(int graceLoginsRemaining);
305
}
306
```
307
308
### PasswordPolicyControlFactory
309
310
Factory for creating password policy controls from LDAP control responses.
311
312
```java { .api }
313
/**
314
* Factory class for creating password policy controls from LDAP responses
315
*/
316
public class PasswordPolicyControlFactory extends ControlFactory {
317
/**
318
* Creates a control from the provided control information
319
* @param ctl the control to process
320
* @return PasswordPolicyResponseControl if applicable, otherwise the original control
321
* @throws NamingException if control creation fails
322
*/
323
@Override
324
public Control getControlInstance(Control ctl) throws NamingException;
325
}
326
```
327
328
### PasswordPolicyControlExtractor
329
330
Utility class for extracting password policy information from LDAP response controls.
331
332
```java { .api }
333
/**
334
* Utility class for extracting password policy response information from LDAP controls
335
*/
336
public final class PasswordPolicyControlExtractor {
337
/**
338
* Extracts password policy response information from an LDAP response control
339
* @param control the password policy response control
340
* @return PasswordPolicyResponse containing policy information
341
* @throws PasswordPolicyException if policy violations are detected
342
*/
343
public static PasswordPolicyResponse extractPasswordPolicyResponse(PasswordPolicyResponseControl control)
344
throws PasswordPolicyException;
345
346
/**
347
* Checks LDAP response controls for password policy information
348
* @param responseControls array of LDAP response controls
349
* @return PasswordPolicyResponse if policy control found, null otherwise
350
* @throws PasswordPolicyException if policy violations are detected
351
*/
352
public static PasswordPolicyResponse checkForPasswordPolicyControl(Control[] responseControls)
353
throws PasswordPolicyException;
354
}
355
```
356
357
## Password Policy Integration
358
359
### Enhanced Authentication with Policy Support
360
361
```java { .api }
362
/**
363
* Enhanced LDAP authenticator that processes password policy controls
364
*/
365
public class PolicyAwareBindAuthenticator extends BindAuthenticator {
366
367
/**
368
* Creates a policy-aware bind authenticator
369
* @param contextSource the LDAP context source
370
*/
371
public PolicyAwareBindAuthenticator(ContextSource contextSource);
372
373
/**
374
* Authenticates with password policy control processing
375
* @param authentication the authentication request
376
* @return DirContextOperations with policy information
377
* @throws PasswordPolicyException if policy violations occur
378
*/
379
@Override
380
public DirContextOperations authenticate(Authentication authentication) {
381
String username = authentication.getName();
382
String password = (String) authentication.getCredentials();
383
384
try {
385
// Get user DN
386
String userDn = getUserDn(username);
387
388
// Create context with password policy control
389
LdapContext ctx = (LdapContext) getContextSource().getContext(userDn, password);
390
Control[] requestControls = { new PasswordPolicyControl() };
391
ctx.setRequestControls(requestControls);
392
393
// Perform a simple operation to trigger policy evaluation
394
ctx.getAttributes("", new String[]{"1.1"});
395
396
// Check for password policy response
397
Control[] responseControls = ctx.getResponseControls();
398
if (responseControls != null) {
399
PasswordPolicyResponse response =
400
PasswordPolicyControlExtractor.checkForPasswordPolicyControl(responseControls);
401
402
if (response != null) {
403
processPasswordPolicyResponse(response, username);
404
}
405
}
406
407
// Return user context information
408
return createUserContext(ctx, username);
409
410
} catch (NamingException e) {
411
throw new BadCredentialsException("Authentication failed", e);
412
}
413
}
414
415
private void processPasswordPolicyResponse(PasswordPolicyResponse response, String username) {
416
// Handle password policy warnings
417
if (response.getTimeBeforeExpiration() > 0) {
418
logger.info("Password expires in {} seconds for user: {}",
419
response.getTimeBeforeExpiration(), username);
420
}
421
422
if (response.getGraceAuthNsRemaining() > 0) {
423
logger.warn("User {} has {} grace logins remaining",
424
username, response.getGraceAuthNsRemaining());
425
}
426
427
// Handle password policy errors
428
if (response.hasError()) {
429
PasswordPolicyErrorStatus error = response.getErrorStatus();
430
throw new PasswordPolicyException(error,
431
"Password policy violation: " + error.name());
432
}
433
}
434
}
435
```
436
437
### Password Policy Response Processing
438
439
```java { .api }
440
/**
441
* Container for password policy response information
442
*/
443
public class PasswordPolicyResponse {
444
private final int timeBeforeExpiration;
445
private final int graceAuthNsRemaining;
446
private final PasswordPolicyErrorStatus errorStatus;
447
448
/**
449
* Creates a password policy response
450
* @param timeBeforeExpiration seconds until password expires (0 if not applicable)
451
* @param graceAuthNsRemaining number of grace authentications remaining
452
* @param errorStatus error status, or null if no error
453
*/
454
public PasswordPolicyResponse(int timeBeforeExpiration, int graceAuthNsRemaining,
455
PasswordPolicyErrorStatus errorStatus);
456
457
/**
458
* Gets the time in seconds before password expiration
459
* @return seconds until expiration, or 0 if not applicable
460
*/
461
public int getTimeBeforeExpiration();
462
463
/**
464
* Gets the number of grace authentications remaining
465
* @return number of grace logins, or 0 if not applicable
466
*/
467
public int getGraceAuthNsRemaining();
468
469
/**
470
* Gets the password policy error status
471
* @return error status, or null if no error
472
*/
473
public PasswordPolicyErrorStatus getErrorStatus();
474
475
/**
476
* Indicates whether this response contains an error
477
* @return true if error status is present
478
*/
479
public boolean hasError();
480
481
/**
482
* Indicates whether this response contains warnings
483
* @return true if expiration warning or grace login information present
484
*/
485
public boolean hasWarning();
486
}
487
```
488
489
## Configuration Examples
490
491
### Password Policy-Aware Authentication Provider
492
493
```java
494
@Configuration
495
public class PasswordPolicyConfig {
496
497
@Bean
498
public PolicyAwareBindAuthenticator policyAwareAuthenticator() {
499
PolicyAwareBindAuthenticator authenticator =
500
new PolicyAwareBindAuthenticator(contextSource());
501
authenticator.setUserSearch(userSearch());
502
return authenticator;
503
}
504
505
@Bean
506
public LdapAuthenticationProvider policyAwareAuthProvider() {
507
LdapAuthenticationProvider provider =
508
new LdapAuthenticationProvider(policyAwareAuthenticator());
509
510
// Set custom authentication exception handler
511
provider.setAuthenticationExceptionHandler(passwordPolicyExceptionHandler());
512
513
return provider;
514
}
515
516
@Bean
517
public AuthenticationExceptionHandler passwordPolicyExceptionHandler() {
518
return new PasswordPolicyAuthenticationExceptionHandler();
519
}
520
}
521
```
522
523
### Custom Password Policy Exception Handler
524
525
```java
526
@Component
527
public class PasswordPolicyAuthenticationExceptionHandler {
528
529
private static final Logger logger = LoggerFactory.getLogger(
530
PasswordPolicyAuthenticationExceptionHandler.class);
531
532
public void handlePasswordPolicyException(PasswordPolicyException ex, String username) {
533
PasswordPolicyErrorStatus status = ex.getStatus();
534
535
switch (status) {
536
case PASSWORD_EXPIRED:
537
logger.warn("Password expired for user: {}", username);
538
// Redirect to password change page
539
break;
540
541
case ACCOUNT_LOCKED:
542
logger.warn("Account locked for user: {}", username);
543
// Send account locked notification
544
break;
545
546
case CHANGE_AFTER_RESET:
547
logger.info("User {} must change password after reset", username);
548
// Force password change workflow
549
break;
550
551
case INSUFFICIENT_PASSWORD_QUALITY:
552
logger.info("Password quality insufficient for user: {}", username);
553
// Show password requirements
554
break;
555
556
case PASSWORD_TOO_SHORT:
557
logger.info("Password too short for user: {}", username);
558
// Show minimum length requirement
559
break;
560
561
default:
562
logger.error("Password policy violation for user {}: {}", username, status);
563
break;
564
}
565
}
566
}
567
```
568
569
### Web Security Integration
570
571
```java
572
@Configuration
573
@EnableWebSecurity
574
public class WebSecurityConfig {
575
576
@Bean
577
public AuthenticationFailureHandler ldapAuthenticationFailureHandler() {
578
return new LdapPasswordPolicyAuthenticationFailureHandler();
579
}
580
581
@Bean
582
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
583
http
584
.authorizeHttpRequests(authz -> authz
585
.requestMatchers("/login", "/password-change").permitAll()
586
.anyRequest().authenticated()
587
)
588
.formLogin(form -> form
589
.loginPage("/login")
590
.failureHandler(ldapAuthenticationFailureHandler())
591
.permitAll()
592
)
593
.logout(logout -> logout.permitAll());
594
595
return http.build();
596
}
597
}
598
599
@Component
600
public class LdapPasswordPolicyAuthenticationFailureHandler
601
implements AuthenticationFailureHandler {
602
603
@Override
604
public void onAuthenticationFailure(HttpServletRequest request,
605
HttpServletResponse response, AuthenticationException exception)
606
throws IOException, ServletException {
607
608
if (exception instanceof PasswordPolicyException) {
609
PasswordPolicyException ppEx = (PasswordPolicyException) exception;
610
PasswordPolicyErrorStatus status = ppEx.getStatus();
611
612
switch (status) {
613
case PASSWORD_EXPIRED:
614
case CHANGE_AFTER_RESET:
615
response.sendRedirect("/password-change?expired=true");
616
return;
617
618
case ACCOUNT_LOCKED:
619
response.sendRedirect("/login?locked=true");
620
return;
621
622
default:
623
break;
624
}
625
}
626
627
// Default failure handling
628
response.sendRedirect("/login?error=true");
629
}
630
}
631
```
632
633
### Password Change Controller
634
635
```java
636
@Controller
637
public class PasswordChangeController {
638
639
private final LdapTemplate ldapTemplate;
640
private final PasswordEncoder passwordEncoder;
641
642
public PasswordChangeController(LdapTemplate ldapTemplate, PasswordEncoder passwordEncoder) {
643
this.ldapTemplate = ldapTemplate;
644
this.passwordEncoder = passwordEncoder;
645
}
646
647
@GetMapping("/password-change")
648
public String showPasswordChangeForm(Model model,
649
@RequestParam(required = false) String expired) {
650
if ("true".equals(expired)) {
651
model.addAttribute("message", "Your password has expired and must be changed.");
652
}
653
return "password-change";
654
}
655
656
@PostMapping("/password-change")
657
public String changePassword(@RequestParam String currentPassword,
658
@RequestParam String newPassword,
659
@RequestParam String confirmPassword,
660
Authentication authentication,
661
RedirectAttributes redirectAttributes) {
662
663
try {
664
if (!newPassword.equals(confirmPassword)) {
665
redirectAttributes.addFlashAttribute("error", "Passwords do not match");
666
return "redirect:/password-change";
667
}
668
669
String username = authentication.getName();
670
String userDn = findUserDn(username);
671
672
// Validate current password
673
validateCurrentPassword(userDn, currentPassword);
674
675
// Change password with policy compliance check
676
changeUserPassword(userDn, newPassword);
677
678
redirectAttributes.addFlashAttribute("success", "Password changed successfully");
679
return "redirect:/dashboard";
680
681
} catch (PasswordPolicyException ex) {
682
String errorMessage = getPasswordPolicyErrorMessage(ex.getStatus());
683
redirectAttributes.addFlashAttribute("error", errorMessage);
684
return "redirect:/password-change";
685
686
} catch (Exception ex) {
687
redirectAttributes.addFlashAttribute("error", "Password change failed");
688
return "redirect:/password-change";
689
}
690
}
691
692
private void changeUserPassword(String userDn, String newPassword) {
693
// Use LDAP modify operation with password policy control
694
LdapContext ctx = (LdapContext) ldapTemplate.getContextSource().getContext(
695
"cn=admin,dc=example,dc=com", "adminPassword");
696
697
try {
698
Control[] requestControls = { new PasswordPolicyControl() };
699
ctx.setRequestControls(requestControls);
700
701
ModificationItem[] mods = new ModificationItem[] {
702
new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
703
new BasicAttribute("userPassword", passwordEncoder.encode(newPassword)))
704
};
705
706
ctx.modifyAttributes(userDn, mods);
707
708
// Check response for policy violations
709
Control[] responseControls = ctx.getResponseControls();
710
PasswordPolicyControlExtractor.checkForPasswordPolicyControl(responseControls);
711
712
} catch (NamingException ex) {
713
throw new RuntimeException("Failed to change password", ex);
714
} finally {
715
LdapUtils.closeContext(ctx);
716
}
717
}
718
719
private String getPasswordPolicyErrorMessage(PasswordPolicyErrorStatus status) {
720
switch (status) {
721
case INSUFFICIENT_PASSWORD_QUALITY:
722
return "Password does not meet quality requirements";
723
case PASSWORD_TOO_SHORT:
724
return "Password is too short";
725
case PASSWORD_IN_HISTORY:
726
return "Password was used recently and cannot be reused";
727
case PASSWORD_TOO_YOUNG:
728
return "Password was changed too recently";
729
default:
730
return "Password policy violation: " + status.name();
731
}
732
}
733
}
734
```