0
# Password Policies
1
2
Password policies in CAS provide comprehensive password management capabilities including expiration warnings, complexity validation, account state handling, and password encoding. The authentication API supports configurable password policy enforcement that integrates with various authentication sources and directory services.
3
4
## Core Password Policy Interface
5
6
```java { .api }
7
package org.apereo.cas.authentication;
8
9
import org.apereo.cas.authentication.principal.Principal;
10
import java.util.List;
11
12
public interface AuthenticationPasswordPolicyHandlingStrategy<AuthnResponse, PasswordPolicyConfiguration> {
13
List<MessageDescriptor> handle(AuthnResponse response, PasswordPolicyConfiguration configuration)
14
throws Throwable;
15
}
16
```
17
18
## Password Policy Context
19
20
```java { .api }
21
package org.apereo.cas.authentication.support.password;
22
23
import org.apereo.cas.authentication.AuthenticationAccountStateHandler;
24
import org.apereo.cas.configuration.model.core.authentication.PasswordPolicyProperties;
25
26
public class PasswordPolicyContext {
27
private AuthenticationAccountStateHandler accountStateHandler;
28
private boolean alwaysDisplayPasswordExpirationWarning;
29
private int passwordWarningNumberOfDays = 30;
30
private int loginFailures = 5;
31
32
// Constructors
33
public PasswordPolicyContext() {}
34
35
public PasswordPolicyContext(int passwordWarningNumberOfDays) {
36
this.passwordWarningNumberOfDays = passwordWarningNumberOfDays;
37
}
38
39
public PasswordPolicyContext(AuthenticationAccountStateHandler accountStateHandler,
40
boolean alwaysDisplayPasswordExpirationWarning,
41
int passwordWarningNumberOfDays,
42
int loginFailures) {
43
this.accountStateHandler = accountStateHandler;
44
this.alwaysDisplayPasswordExpirationWarning = alwaysDisplayPasswordExpirationWarning;
45
this.passwordWarningNumberOfDays = passwordWarningNumberOfDays;
46
this.loginFailures = loginFailures;
47
}
48
49
public PasswordPolicyContext(PasswordPolicyProperties props) {
50
this(null, props.isWarnAll(), props.getWarningDays(), props.getLoginFailures());
51
}
52
53
// Getters and setters
54
public AuthenticationAccountStateHandler getAccountStateHandler() { return accountStateHandler; }
55
public void setAccountStateHandler(AuthenticationAccountStateHandler accountStateHandler) {
56
this.accountStateHandler = accountStateHandler;
57
}
58
59
public boolean isAlwaysDisplayPasswordExpirationWarning() {
60
return alwaysDisplayPasswordExpirationWarning;
61
}
62
public void setAlwaysDisplayPasswordExpirationWarning(boolean alwaysDisplayPasswordExpirationWarning) {
63
this.alwaysDisplayPasswordExpirationWarning = alwaysDisplayPasswordExpirationWarning;
64
}
65
66
public int getPasswordWarningNumberOfDays() { return passwordWarningNumberOfDays; }
67
public void setPasswordWarningNumberOfDays(int passwordWarningNumberOfDays) {
68
this.passwordWarningNumberOfDays = passwordWarningNumberOfDays;
69
}
70
71
public int getLoginFailures() { return loginFailures; }
72
public void setLoginFailures(int loginFailures) { this.loginFailures = loginFailures; }
73
}
74
```
75
76
## Message Descriptors
77
78
### Password Expiring Warning Message
79
80
```java { .api }
81
package org.apereo.cas.authentication.support.password;
82
83
import org.apereo.cas.authentication.MessageDescriptor;
84
import java.time.ZonedDateTime;
85
86
public class PasswordExpiringWarningMessageDescriptor implements MessageDescriptor {
87
88
public static final String DEFAULT_CODE = "password.expiration.warning";
89
90
private final String code;
91
private final String defaultMessage;
92
private final int daysToExpiration;
93
private final ZonedDateTime expirationDate;
94
95
public PasswordExpiringWarningMessageDescriptor(String defaultMessage,
96
int daysToExpiration) {
97
this(DEFAULT_CODE, defaultMessage, daysToExpiration, null);
98
}
99
100
public PasswordExpiringWarningMessageDescriptor(String code,
101
String defaultMessage,
102
int daysToExpiration,
103
ZonedDateTime expirationDate) {
104
this.code = code;
105
this.defaultMessage = defaultMessage;
106
this.daysToExpiration = daysToExpiration;
107
this.expirationDate = expirationDate;
108
}
109
110
public String getCode() { return code; }
111
112
public String getDefaultMessage() { return defaultMessage; }
113
114
public Object[] getParams() {
115
return new Object[]{daysToExpiration, expirationDate};
116
}
117
118
public int getDaysToExpiration() { return daysToExpiration; }
119
120
public ZonedDateTime getExpirationDate() { return expirationDate; }
121
}
122
```
123
124
## Password Policy Handling Strategies
125
126
### Default Password Policy Handling Strategy
127
128
```java { .api }
129
package org.apereo.cas.authentication.support.password;
130
131
import org.apereo.cas.authentication.AuthenticationPasswordPolicyHandlingStrategy;
132
import org.apereo.cas.authentication.MessageDescriptor;
133
import java.util.ArrayList;
134
import java.util.List;
135
136
public class DefaultPasswordPolicyHandlingStrategy<AuthnResponse>
137
implements AuthenticationPasswordPolicyHandlingStrategy<AuthnResponse, PasswordPolicyContext> {
138
139
@Override
140
public List<MessageDescriptor> handle(AuthnResponse response,
141
PasswordPolicyContext configuration) throws Throwable {
142
143
if (configuration == null) {
144
return new ArrayList<>();
145
}
146
147
AuthenticationAccountStateHandler accountStateHandler = configuration.getAccountStateHandler();
148
if (accountStateHandler == null) {
149
return new ArrayList<>();
150
}
151
152
return accountStateHandler.handle(response, configuration);
153
}
154
}
155
```
156
157
### Reject Result Code Password Policy Strategy
158
159
Strategy that handles specific LDAP result codes for password policy enforcement:
160
161
```java { .api }
162
package org.apereo.cas.authentication.support.password;
163
164
import org.apereo.cas.authentication.AuthenticationPasswordPolicyHandlingStrategy;
165
import org.apereo.cas.authentication.MessageDescriptor;
166
import org.apereo.cas.authentication.exceptions.AccountDisabledException;
167
import org.apereo.cas.authentication.exceptions.AccountPasswordMustChangeException;
168
import org.apereo.cas.authentication.exceptions.InvalidLoginLocationException;
169
import org.apereo.cas.authentication.exceptions.InvalidLoginTimeException;
170
import java.util.List;
171
import java.util.Map;
172
173
public class RejectResultCodePasswordPolicyHandlingStrategy
174
implements AuthenticationPasswordPolicyHandlingStrategy<Object, PasswordPolicyContext> {
175
176
private static final Map<String, Class<? extends Exception>> RESULT_CODE_EXCEPTIONS = Map.of(
177
"ACCOUNT_DISABLED", AccountDisabledException.class,
178
"PASSWORD_MUST_CHANGE", AccountPasswordMustChangeException.class,
179
"INVALID_LOGIN_LOCATION", InvalidLoginLocationException.class,
180
"INVALID_LOGIN_TIME", InvalidLoginTimeException.class
181
);
182
183
@Override
184
public List<MessageDescriptor> handle(Object response, PasswordPolicyContext configuration)
185
throws Throwable {
186
187
String resultCode = extractResultCode(response);
188
189
if (resultCode != null && RESULT_CODE_EXCEPTIONS.containsKey(resultCode)) {
190
Class<? extends Exception> exceptionClass = RESULT_CODE_EXCEPTIONS.get(resultCode);
191
Exception exception = exceptionClass.getDeclaredConstructor(String.class)
192
.newInstance("Password policy violation: " + resultCode);
193
throw exception;
194
}
195
196
return List.of();
197
}
198
199
private String extractResultCode(Object response) {
200
// Extract result code from authentication response
201
// Implementation depends on the specific authentication backend
202
return null;
203
}
204
}
205
```
206
207
### Groovy Password Policy Handling Strategy
208
209
Scriptable password policy handler using Groovy:
210
211
```java { .api }
212
package org.apereo.cas.authentication.support.password;
213
214
import org.apereo.cas.authentication.AuthenticationPasswordPolicyHandlingStrategy;
215
import org.apereo.cas.authentication.MessageDescriptor;
216
import org.apereo.cas.util.scripting.ExecutableCompiledGroovyScript;
217
import java.util.ArrayList;
218
import java.util.List;
219
220
public class GroovyPasswordPolicyHandlingStrategy
221
implements AuthenticationPasswordPolicyHandlingStrategy<Object, PasswordPolicyContext> {
222
223
private final ExecutableCompiledGroovyScript watchableScript;
224
225
public GroovyPasswordPolicyHandlingStrategy(ExecutableCompiledGroovyScript watchableScript) {
226
this.watchableScript = watchableScript;
227
}
228
229
@Override
230
public List<MessageDescriptor> handle(Object response, PasswordPolicyContext configuration)
231
throws Throwable {
232
233
Object result = watchableScript.execute(response, configuration, List.class);
234
235
if (result instanceof List) {
236
return (List<MessageDescriptor>) result;
237
}
238
239
return new ArrayList<>();
240
}
241
}
242
```
243
244
## Account State Handlers
245
246
```java { .api }
247
package org.apereo.cas.authentication;
248
249
import org.apereo.cas.authentication.support.password.PasswordPolicyContext;
250
import java.util.List;
251
252
public interface AuthenticationAccountStateHandler<Response, Configuration> {
253
List<MessageDescriptor> handle(Response response, Configuration configuration) throws Exception;
254
255
boolean supports(Response response);
256
}
257
```
258
259
### LDAP Account State Handler
260
261
```java { .api }
262
package org.apereo.cas.authentication.support.password;
263
264
import org.apereo.cas.authentication.AuthenticationAccountStateHandler;
265
import org.apereo.cas.authentication.MessageDescriptor;
266
import java.time.LocalDateTime;
267
import java.time.temporal.ChronoUnit;
268
import java.util.ArrayList;
269
import java.util.List;
270
271
public class LdapPasswordPolicyAccountStateHandler
272
implements AuthenticationAccountStateHandler<LdapAuthenticationResult, PasswordPolicyContext> {
273
274
@Override
275
public List<MessageDescriptor> handle(LdapAuthenticationResult response,
276
PasswordPolicyContext configuration) throws Exception {
277
278
List<MessageDescriptor> messages = new ArrayList<>();
279
280
// Check password expiration
281
LocalDateTime expirationDate = response.getPasswordExpirationDate();
282
if (expirationDate != null) {
283
long daysToExpiration = ChronoUnit.DAYS.between(LocalDateTime.now(), expirationDate);
284
285
if (daysToExpiration <= configuration.getPasswordWarningNumberOfDays()) {
286
messages.add(new PasswordExpiringWarningMessageDescriptor(
287
"Your password expires in " + daysToExpiration + " days",
288
(int) daysToExpiration));
289
}
290
}
291
292
// Check account locked status
293
if (response.isAccountLocked()) {
294
throw new AccountLockedException("Account is locked");
295
}
296
297
// Check if password must be changed
298
if (response.isPasswordMustChange()) {
299
throw new AccountPasswordMustChangeException("Password must be changed");
300
}
301
302
// Check login failures
303
int failureCount = response.getLoginFailureCount();
304
if (failureCount >= configuration.getLoginFailures()) {
305
throw new AccountLockedException("Too many login failures: " + failureCount);
306
}
307
308
return messages;
309
}
310
311
@Override
312
public boolean supports(LdapAuthenticationResult response) {
313
return response != null;
314
}
315
}
316
```
317
318
## Password Encoding
319
320
### Password Encoder Interface
321
322
```java { .api }
323
package org.springframework.security.crypto.password;
324
325
public interface PasswordEncoder {
326
String encode(CharSequence rawPassword);
327
boolean matches(CharSequence rawPassword, String encodedPassword);
328
default boolean upgradeEncoding(String encodedPassword) { return false; }
329
}
330
```
331
332
### Password Encoder Utilities
333
334
```java { .api }
335
package org.apereo.cas.authentication.support.password;
336
337
import org.apereo.cas.configuration.model.core.authentication.PasswordEncoderProperties;
338
import org.springframework.context.ApplicationContext;
339
import org.springframework.security.crypto.password.PasswordEncoder;
340
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
341
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
342
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
343
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
344
345
public final class PasswordEncoderUtils {
346
347
private PasswordEncoderUtils() {}
348
349
public static PasswordEncoder newPasswordEncoder(PasswordEncoderProperties properties,
350
ApplicationContext applicationContext) {
351
352
String type = properties.getType();
353
354
switch (type.toLowerCase()) {
355
case "bcrypt":
356
return new BCryptPasswordEncoder(properties.getStrength());
357
358
case "scrypt":
359
return new SCryptPasswordEncoder(
360
properties.getCpuCost(),
361
properties.getMemoryCost(),
362
properties.getParallelization(),
363
properties.getKeyLength(),
364
properties.getSaltLength());
365
366
case "argon2":
367
return new Argon2PasswordEncoder(
368
properties.getSaltLength(),
369
properties.getHashLength(),
370
properties.getParallelism(),
371
properties.getMemory(),
372
properties.getIterations());
373
374
case "pbkdf2":
375
return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
376
377
case "noop":
378
return NoOpPasswordEncoder.getInstance();
379
380
case "groovy":
381
return new GroovyPasswordEncoder(properties, applicationContext);
382
383
default:
384
throw new IllegalArgumentException("Unsupported password encoder type: " + type);
385
}
386
}
387
}
388
```
389
390
### Groovy Password Encoder
391
392
Custom password encoder using Groovy scripts:
393
394
```java { .api }
395
package org.apereo.cas.authentication.support.password;
396
397
import org.apereo.cas.configuration.model.core.authentication.PasswordEncoderProperties;
398
import org.apereo.cas.util.scripting.ExecutableCompiledGroovyScript;
399
import org.springframework.context.ApplicationContext;
400
import org.springframework.security.crypto.password.PasswordEncoder;
401
402
public class GroovyPasswordEncoder implements PasswordEncoder {
403
404
private final ExecutableCompiledGroovyScript encodeScript;
405
private final ExecutableCompiledGroovyScript matchesScript;
406
407
public GroovyPasswordEncoder(PasswordEncoderProperties properties,
408
ApplicationContext applicationContext) {
409
410
this.encodeScript = ExecutableCompiledGroovyScript.getExecutableCompiledGroovyScript(
411
properties.getEncodeScript(), applicationContext);
412
413
this.matchesScript = ExecutableCompiledGroovyScript.getExecutableCompiledGroovyScript(
414
properties.getMatchesScript(), applicationContext);
415
}
416
417
@Override
418
public String encode(CharSequence rawPassword) {
419
Object result = encodeScript.execute(rawPassword.toString(), String.class);
420
return result != null ? result.toString() : null;
421
}
422
423
@Override
424
public boolean matches(CharSequence rawPassword, String encodedPassword) {
425
Object result = matchesScript.execute(rawPassword.toString(), encodedPassword, Boolean.class);
426
return result instanceof Boolean ? (Boolean) result : false;
427
}
428
429
@Override
430
public boolean upgradeEncoding(String encodedPassword) {
431
return false;
432
}
433
}
434
```
435
436
## Password Policy Configuration
437
438
### Configuration Properties
439
440
```java { .api }
441
package org.apereo.cas.configuration.model.core.authentication;
442
443
import java.io.Serializable;
444
445
public class PasswordPolicyProperties implements Serializable {
446
447
private boolean enabled = false;
448
private boolean warnAll = false;
449
private boolean displayWarningOnFailedLogin = true;
450
private boolean displayWarningOnSuccessfulLogin = true;
451
private int warningDays = 30;
452
private int loginFailures = 5;
453
private String type = "DEFAULT";
454
private String groovyScript;
455
private String endpoint;
456
457
// Account state handling
458
private boolean accountStateHandlingEnabled = true;
459
private String accountStateHandler = "DEFAULT";
460
461
// Password encoding
462
private PasswordEncoderProperties passwordEncoder = new PasswordEncoderProperties();
463
464
// Getters and setters
465
public boolean isEnabled() { return enabled; }
466
public void setEnabled(boolean enabled) { this.enabled = enabled; }
467
468
public boolean isWarnAll() { return warnAll; }
469
public void setWarnAll(boolean warnAll) { this.warnAll = warnAll; }
470
471
public boolean isDisplayWarningOnFailedLogin() { return displayWarningOnFailedLogin; }
472
public void setDisplayWarningOnFailedLogin(boolean displayWarningOnFailedLogin) {
473
this.displayWarningOnFailedLogin = displayWarningOnFailedLogin;
474
}
475
476
public boolean isDisplayWarningOnSuccessfulLogin() { return displayWarningOnSuccessfulLogin; }
477
public void setDisplayWarningOnSuccessfulLogin(boolean displayWarningOnSuccessfulLogin) {
478
this.displayWarningOnSuccessfulLogin = displayWarningOnSuccessfulLogin;
479
}
480
481
public int getWarningDays() { return warningDays; }
482
public void setWarningDays(int warningDays) { this.warningDays = warningDays; }
483
484
public int getLoginFailures() { return loginFailures; }
485
public void setLoginFailures(int loginFailures) { this.loginFailures = loginFailures; }
486
487
// Additional getters and setters...
488
}
489
```
490
491
### Password Encoder Properties
492
493
```java { .api }
494
package org.apereo.cas.configuration.model.core.authentication;
495
496
public class PasswordEncoderProperties implements Serializable {
497
498
private String type = "NONE";
499
private String characterEncoding = "UTF-8";
500
private String encodingAlgorithm = "SHA-256";
501
private int strength = 10;
502
private boolean secret = false;
503
504
// BCrypt specific
505
private int cost = 10;
506
507
// SCrypt specific
508
private int cpuCost = 16384;
509
private int memoryCost = 8;
510
private int parallelization = 1;
511
private int keyLength = 32;
512
private int saltLength = 16;
513
514
// Argon2 specific
515
private int hashLength = 32;
516
private int parallelism = 1;
517
private int memory = 4096;
518
private int iterations = 3;
519
520
// Groovy specific
521
private String encodeScript;
522
private String matchesScript;
523
524
// Getters and setters
525
public String getType() { return type; }
526
public void setType(String type) { this.type = type; }
527
528
public int getStrength() { return strength; }
529
public void setStrength(int strength) { this.strength = strength; }
530
531
// Additional getters and setters...
532
}
533
```
534
535
## Integration Examples
536
537
### Spring Configuration
538
539
```java { .api }
540
@Configuration
541
@EnableConfigurationProperties(CasConfigurationProperties.class)
542
public class PasswordPolicyConfiguration {
543
544
@Bean
545
public PasswordPolicyContext passwordPolicyContext(CasConfigurationProperties casProperties) {
546
PasswordPolicyProperties props = casProperties.getAuthn().getPasswordPolicy();
547
return new PasswordPolicyContext(props);
548
}
549
550
@Bean
551
public AuthenticationPasswordPolicyHandlingStrategy passwordPolicyHandlingStrategy() {
552
return new DefaultPasswordPolicyHandlingStrategy<>();
553
}
554
555
@Bean
556
public PasswordEncoder passwordEncoder(CasConfigurationProperties casProperties,
557
ApplicationContext applicationContext) {
558
PasswordEncoderProperties props = casProperties.getAuthn().getPasswordEncoder();
559
return PasswordEncoderUtils.newPasswordEncoder(props, applicationContext);
560
}
561
562
@Bean
563
public AuthenticationAccountStateHandler accountStateHandler() {
564
return new LdapPasswordPolicyAccountStateHandler();
565
}
566
}
567
```
568
569
### Programmatic Usage
570
571
```java { .api }
572
// Create password policy context
573
PasswordPolicyContext policyContext = new PasswordPolicyContext();
574
policyContext.setPasswordWarningNumberOfDays(30);
575
policyContext.setLoginFailures(5);
576
policyContext.setAlwaysDisplayPasswordExpirationWarning(false);
577
578
// Set up account state handler
579
AuthenticationAccountStateHandler accountStateHandler = new LdapPasswordPolicyAccountStateHandler();
580
policyContext.setAccountStateHandler(accountStateHandler);
581
582
// Create password policy handling strategy
583
AuthenticationPasswordPolicyHandlingStrategy strategy = new DefaultPasswordPolicyHandlingStrategy<>();
584
585
// Handle password policy during authentication
586
try {
587
List<MessageDescriptor> messages = strategy.handle(authenticationResponse, policyContext);
588
589
for (MessageDescriptor message : messages) {
590
if (message instanceof PasswordExpiringWarningMessageDescriptor) {
591
PasswordExpiringWarningMessageDescriptor warning =
592
(PasswordExpiringWarningMessageDescriptor) message;
593
System.out.println("Password expires in " + warning.getDaysToExpiration() + " days");
594
}
595
}
596
} catch (AccountPasswordMustChangeException e) {
597
// Redirect user to password change form
598
System.out.println("Password must be changed");
599
} catch (AccountLockedException e) {
600
// Display account locked message
601
System.out.println("Account is locked");
602
}
603
604
// Password encoding example
605
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
606
String encodedPassword = encoder.encode("plainTextPassword");
607
boolean matches = encoder.matches("plainTextPassword", encodedPassword);
608
609
// Groovy password policy example
610
String groovyScript = """
611
import org.apereo.cas.authentication.MessageDescriptor
612
import org.apereo.cas.authentication.support.password.PasswordExpiringWarningMessageDescriptor
613
614
def handle(response, configuration) {
615
def messages = []
616
617
if (response.passwordExpirationDays <= 7) {
618
messages.add(new PasswordExpiringWarningMessageDescriptor(
619
"Password expires soon!",
620
response.passwordExpirationDays))
621
}
622
623
return messages
624
}
625
626
handle(binding.variables.response, binding.variables.configuration)
627
""";
628
629
ExecutableCompiledGroovyScript script = new ExecutableCompiledGroovyScript(groovyScript);
630
GroovyPasswordPolicyHandlingStrategy groovyStrategy = new GroovyPasswordPolicyHandlingStrategy(script);
631
```
632
633
### Custom Password Policy Implementation
634
635
```java { .api }
636
public class CustomPasswordPolicyHandlingStrategy
637
implements AuthenticationPasswordPolicyHandlingStrategy<CustomAuthResponse, PasswordPolicyContext> {
638
639
@Override
640
public List<MessageDescriptor> handle(CustomAuthResponse response,
641
PasswordPolicyContext configuration) throws Throwable {
642
643
List<MessageDescriptor> messages = new ArrayList<>();
644
645
// Check custom password requirements
646
if (response.isPasswordWeak()) {
647
messages.add(new MessageDescriptor() {
648
public String getCode() { return "password.weak"; }
649
public String getDefaultMessage() { return "Password is too weak"; }
650
public Object[] getParams() { return new Object[0]; }
651
});
652
}
653
654
// Check password age
655
int passwordAge = response.getPasswordAgeDays();
656
if (passwordAge > 90) {
657
messages.add(new PasswordExpiringWarningMessageDescriptor(
658
"Password is old and should be changed",
659
90 - passwordAge));
660
}
661
662
// Check account inactivity
663
if (response.getDaysSinceLastLogin() > 180) {
664
throw new AccountDisabledException("Account inactive for too long");
665
}
666
667
return messages;
668
}
669
}
670
```
671
672
Password policies provide comprehensive password management and security enforcement, enabling organizations to implement sophisticated password requirements, expiration warnings, and account state management integrated with their authentication infrastructure.