Core authentication API module for Apereo CAS providing fundamental authentication interfaces, implementations, and components for the CAS server infrastructure
—
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.
package org.apereo.cas.authentication;
import org.apereo.cas.authentication.principal.Principal;
import java.util.List;
public interface AuthenticationPasswordPolicyHandlingStrategy<AuthnResponse, PasswordPolicyConfiguration> {
List<MessageDescriptor> handle(AuthnResponse response, PasswordPolicyConfiguration configuration)
throws Throwable;
}package org.apereo.cas.authentication.support.password;
import org.apereo.cas.authentication.AuthenticationAccountStateHandler;
import org.apereo.cas.configuration.model.core.authentication.PasswordPolicyProperties;
public class PasswordPolicyContext {
private AuthenticationAccountStateHandler accountStateHandler;
private boolean alwaysDisplayPasswordExpirationWarning;
private int passwordWarningNumberOfDays = 30;
private int loginFailures = 5;
// Constructors
public PasswordPolicyContext() {}
public PasswordPolicyContext(int passwordWarningNumberOfDays) {
this.passwordWarningNumberOfDays = passwordWarningNumberOfDays;
}
public PasswordPolicyContext(AuthenticationAccountStateHandler accountStateHandler,
boolean alwaysDisplayPasswordExpirationWarning,
int passwordWarningNumberOfDays,
int loginFailures) {
this.accountStateHandler = accountStateHandler;
this.alwaysDisplayPasswordExpirationWarning = alwaysDisplayPasswordExpirationWarning;
this.passwordWarningNumberOfDays = passwordWarningNumberOfDays;
this.loginFailures = loginFailures;
}
public PasswordPolicyContext(PasswordPolicyProperties props) {
this(null, props.isWarnAll(), props.getWarningDays(), props.getLoginFailures());
}
// Getters and setters
public AuthenticationAccountStateHandler getAccountStateHandler() { return accountStateHandler; }
public void setAccountStateHandler(AuthenticationAccountStateHandler accountStateHandler) {
this.accountStateHandler = accountStateHandler;
}
public boolean isAlwaysDisplayPasswordExpirationWarning() {
return alwaysDisplayPasswordExpirationWarning;
}
public void setAlwaysDisplayPasswordExpirationWarning(boolean alwaysDisplayPasswordExpirationWarning) {
this.alwaysDisplayPasswordExpirationWarning = alwaysDisplayPasswordExpirationWarning;
}
public int getPasswordWarningNumberOfDays() { return passwordWarningNumberOfDays; }
public void setPasswordWarningNumberOfDays(int passwordWarningNumberOfDays) {
this.passwordWarningNumberOfDays = passwordWarningNumberOfDays;
}
public int getLoginFailures() { return loginFailures; }
public void setLoginFailures(int loginFailures) { this.loginFailures = loginFailures; }
}package org.apereo.cas.authentication.support.password;
import org.apereo.cas.authentication.MessageDescriptor;
import java.time.ZonedDateTime;
public class PasswordExpiringWarningMessageDescriptor implements MessageDescriptor {
public static final String DEFAULT_CODE = "password.expiration.warning";
private final String code;
private final String defaultMessage;
private final int daysToExpiration;
private final ZonedDateTime expirationDate;
public PasswordExpiringWarningMessageDescriptor(String defaultMessage,
int daysToExpiration) {
this(DEFAULT_CODE, defaultMessage, daysToExpiration, null);
}
public PasswordExpiringWarningMessageDescriptor(String code,
String defaultMessage,
int daysToExpiration,
ZonedDateTime expirationDate) {
this.code = code;
this.defaultMessage = defaultMessage;
this.daysToExpiration = daysToExpiration;
this.expirationDate = expirationDate;
}
public String getCode() { return code; }
public String getDefaultMessage() { return defaultMessage; }
public Object[] getParams() {
return new Object[]{daysToExpiration, expirationDate};
}
public int getDaysToExpiration() { return daysToExpiration; }
public ZonedDateTime getExpirationDate() { return expirationDate; }
}package org.apereo.cas.authentication.support.password;
import org.apereo.cas.authentication.AuthenticationPasswordPolicyHandlingStrategy;
import org.apereo.cas.authentication.MessageDescriptor;
import java.util.ArrayList;
import java.util.List;
public class DefaultPasswordPolicyHandlingStrategy<AuthnResponse>
implements AuthenticationPasswordPolicyHandlingStrategy<AuthnResponse, PasswordPolicyContext> {
@Override
public List<MessageDescriptor> handle(AuthnResponse response,
PasswordPolicyContext configuration) throws Throwable {
if (configuration == null) {
return new ArrayList<>();
}
AuthenticationAccountStateHandler accountStateHandler = configuration.getAccountStateHandler();
if (accountStateHandler == null) {
return new ArrayList<>();
}
return accountStateHandler.handle(response, configuration);
}
}Strategy that handles specific LDAP result codes for password policy enforcement:
package org.apereo.cas.authentication.support.password;
import org.apereo.cas.authentication.AuthenticationPasswordPolicyHandlingStrategy;
import org.apereo.cas.authentication.MessageDescriptor;
import org.apereo.cas.authentication.exceptions.AccountDisabledException;
import org.apereo.cas.authentication.exceptions.AccountPasswordMustChangeException;
import org.apereo.cas.authentication.exceptions.InvalidLoginLocationException;
import org.apereo.cas.authentication.exceptions.InvalidLoginTimeException;
import java.util.List;
import java.util.Map;
public class RejectResultCodePasswordPolicyHandlingStrategy
implements AuthenticationPasswordPolicyHandlingStrategy<Object, PasswordPolicyContext> {
private static final Map<String, Class<? extends Exception>> RESULT_CODE_EXCEPTIONS = Map.of(
"ACCOUNT_DISABLED", AccountDisabledException.class,
"PASSWORD_MUST_CHANGE", AccountPasswordMustChangeException.class,
"INVALID_LOGIN_LOCATION", InvalidLoginLocationException.class,
"INVALID_LOGIN_TIME", InvalidLoginTimeException.class
);
@Override
public List<MessageDescriptor> handle(Object response, PasswordPolicyContext configuration)
throws Throwable {
String resultCode = extractResultCode(response);
if (resultCode != null && RESULT_CODE_EXCEPTIONS.containsKey(resultCode)) {
Class<? extends Exception> exceptionClass = RESULT_CODE_EXCEPTIONS.get(resultCode);
Exception exception = exceptionClass.getDeclaredConstructor(String.class)
.newInstance("Password policy violation: " + resultCode);
throw exception;
}
return List.of();
}
private String extractResultCode(Object response) {
// Extract result code from authentication response
// Implementation depends on the specific authentication backend
return null;
}
}Scriptable password policy handler using Groovy:
package org.apereo.cas.authentication.support.password;
import org.apereo.cas.authentication.AuthenticationPasswordPolicyHandlingStrategy;
import org.apereo.cas.authentication.MessageDescriptor;
import org.apereo.cas.util.scripting.ExecutableCompiledGroovyScript;
import java.util.ArrayList;
import java.util.List;
public class GroovyPasswordPolicyHandlingStrategy
implements AuthenticationPasswordPolicyHandlingStrategy<Object, PasswordPolicyContext> {
private final ExecutableCompiledGroovyScript watchableScript;
public GroovyPasswordPolicyHandlingStrategy(ExecutableCompiledGroovyScript watchableScript) {
this.watchableScript = watchableScript;
}
@Override
public List<MessageDescriptor> handle(Object response, PasswordPolicyContext configuration)
throws Throwable {
Object result = watchableScript.execute(response, configuration, List.class);
if (result instanceof List) {
return (List<MessageDescriptor>) result;
}
return new ArrayList<>();
}
}package org.apereo.cas.authentication;
import org.apereo.cas.authentication.support.password.PasswordPolicyContext;
import java.util.List;
public interface AuthenticationAccountStateHandler<Response, Configuration> {
List<MessageDescriptor> handle(Response response, Configuration configuration) throws Exception;
boolean supports(Response response);
}package org.apereo.cas.authentication.support.password;
import org.apereo.cas.authentication.AuthenticationAccountStateHandler;
import org.apereo.cas.authentication.MessageDescriptor;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
public class LdapPasswordPolicyAccountStateHandler
implements AuthenticationAccountStateHandler<LdapAuthenticationResult, PasswordPolicyContext> {
@Override
public List<MessageDescriptor> handle(LdapAuthenticationResult response,
PasswordPolicyContext configuration) throws Exception {
List<MessageDescriptor> messages = new ArrayList<>();
// Check password expiration
LocalDateTime expirationDate = response.getPasswordExpirationDate();
if (expirationDate != null) {
long daysToExpiration = ChronoUnit.DAYS.between(LocalDateTime.now(), expirationDate);
if (daysToExpiration <= configuration.getPasswordWarningNumberOfDays()) {
messages.add(new PasswordExpiringWarningMessageDescriptor(
"Your password expires in " + daysToExpiration + " days",
(int) daysToExpiration));
}
}
// Check account locked status
if (response.isAccountLocked()) {
throw new AccountLockedException("Account is locked");
}
// Check if password must be changed
if (response.isPasswordMustChange()) {
throw new AccountPasswordMustChangeException("Password must be changed");
}
// Check login failures
int failureCount = response.getLoginFailureCount();
if (failureCount >= configuration.getLoginFailures()) {
throw new AccountLockedException("Too many login failures: " + failureCount);
}
return messages;
}
@Override
public boolean supports(LdapAuthenticationResult response) {
return response != null;
}
}package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) { return false; }
}package org.apereo.cas.authentication.support.password;
import org.apereo.cas.configuration.model.core.authentication.PasswordEncoderProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
public final class PasswordEncoderUtils {
private PasswordEncoderUtils() {}
public static PasswordEncoder newPasswordEncoder(PasswordEncoderProperties properties,
ApplicationContext applicationContext) {
String type = properties.getType();
switch (type.toLowerCase()) {
case "bcrypt":
return new BCryptPasswordEncoder(properties.getStrength());
case "scrypt":
return new SCryptPasswordEncoder(
properties.getCpuCost(),
properties.getMemoryCost(),
properties.getParallelization(),
properties.getKeyLength(),
properties.getSaltLength());
case "argon2":
return new Argon2PasswordEncoder(
properties.getSaltLength(),
properties.getHashLength(),
properties.getParallelism(),
properties.getMemory(),
properties.getIterations());
case "pbkdf2":
return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
case "noop":
return NoOpPasswordEncoder.getInstance();
case "groovy":
return new GroovyPasswordEncoder(properties, applicationContext);
default:
throw new IllegalArgumentException("Unsupported password encoder type: " + type);
}
}
}Custom password encoder using Groovy scripts:
package org.apereo.cas.authentication.support.password;
import org.apereo.cas.configuration.model.core.authentication.PasswordEncoderProperties;
import org.apereo.cas.util.scripting.ExecutableCompiledGroovyScript;
import org.springframework.context.ApplicationContext;
import org.springframework.security.crypto.password.PasswordEncoder;
public class GroovyPasswordEncoder implements PasswordEncoder {
private final ExecutableCompiledGroovyScript encodeScript;
private final ExecutableCompiledGroovyScript matchesScript;
public GroovyPasswordEncoder(PasswordEncoderProperties properties,
ApplicationContext applicationContext) {
this.encodeScript = ExecutableCompiledGroovyScript.getExecutableCompiledGroovyScript(
properties.getEncodeScript(), applicationContext);
this.matchesScript = ExecutableCompiledGroovyScript.getExecutableCompiledGroovyScript(
properties.getMatchesScript(), applicationContext);
}
@Override
public String encode(CharSequence rawPassword) {
Object result = encodeScript.execute(rawPassword.toString(), String.class);
return result != null ? result.toString() : null;
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
Object result = matchesScript.execute(rawPassword.toString(), encodedPassword, Boolean.class);
return result instanceof Boolean ? (Boolean) result : false;
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
return false;
}
}package org.apereo.cas.configuration.model.core.authentication;
import java.io.Serializable;
public class PasswordPolicyProperties implements Serializable {
private boolean enabled = false;
private boolean warnAll = false;
private boolean displayWarningOnFailedLogin = true;
private boolean displayWarningOnSuccessfulLogin = true;
private int warningDays = 30;
private int loginFailures = 5;
private String type = "DEFAULT";
private String groovyScript;
private String endpoint;
// Account state handling
private boolean accountStateHandlingEnabled = true;
private String accountStateHandler = "DEFAULT";
// Password encoding
private PasswordEncoderProperties passwordEncoder = new PasswordEncoderProperties();
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public boolean isWarnAll() { return warnAll; }
public void setWarnAll(boolean warnAll) { this.warnAll = warnAll; }
public boolean isDisplayWarningOnFailedLogin() { return displayWarningOnFailedLogin; }
public void setDisplayWarningOnFailedLogin(boolean displayWarningOnFailedLogin) {
this.displayWarningOnFailedLogin = displayWarningOnFailedLogin;
}
public boolean isDisplayWarningOnSuccessfulLogin() { return displayWarningOnSuccessfulLogin; }
public void setDisplayWarningOnSuccessfulLogin(boolean displayWarningOnSuccessfulLogin) {
this.displayWarningOnSuccessfulLogin = displayWarningOnSuccessfulLogin;
}
public int getWarningDays() { return warningDays; }
public void setWarningDays(int warningDays) { this.warningDays = warningDays; }
public int getLoginFailures() { return loginFailures; }
public void setLoginFailures(int loginFailures) { this.loginFailures = loginFailures; }
// Additional getters and setters...
}package org.apereo.cas.configuration.model.core.authentication;
public class PasswordEncoderProperties implements Serializable {
private String type = "NONE";
private String characterEncoding = "UTF-8";
private String encodingAlgorithm = "SHA-256";
private int strength = 10;
private boolean secret = false;
// BCrypt specific
private int cost = 10;
// SCrypt specific
private int cpuCost = 16384;
private int memoryCost = 8;
private int parallelization = 1;
private int keyLength = 32;
private int saltLength = 16;
// Argon2 specific
private int hashLength = 32;
private int parallelism = 1;
private int memory = 4096;
private int iterations = 3;
// Groovy specific
private String encodeScript;
private String matchesScript;
// Getters and setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public int getStrength() { return strength; }
public void setStrength(int strength) { this.strength = strength; }
// Additional getters and setters...
}@Configuration
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class PasswordPolicyConfiguration {
@Bean
public PasswordPolicyContext passwordPolicyContext(CasConfigurationProperties casProperties) {
PasswordPolicyProperties props = casProperties.getAuthn().getPasswordPolicy();
return new PasswordPolicyContext(props);
}
@Bean
public AuthenticationPasswordPolicyHandlingStrategy passwordPolicyHandlingStrategy() {
return new DefaultPasswordPolicyHandlingStrategy<>();
}
@Bean
public PasswordEncoder passwordEncoder(CasConfigurationProperties casProperties,
ApplicationContext applicationContext) {
PasswordEncoderProperties props = casProperties.getAuthn().getPasswordEncoder();
return PasswordEncoderUtils.newPasswordEncoder(props, applicationContext);
}
@Bean
public AuthenticationAccountStateHandler accountStateHandler() {
return new LdapPasswordPolicyAccountStateHandler();
}
}// Create password policy context
PasswordPolicyContext policyContext = new PasswordPolicyContext();
policyContext.setPasswordWarningNumberOfDays(30);
policyContext.setLoginFailures(5);
policyContext.setAlwaysDisplayPasswordExpirationWarning(false);
// Set up account state handler
AuthenticationAccountStateHandler accountStateHandler = new LdapPasswordPolicyAccountStateHandler();
policyContext.setAccountStateHandler(accountStateHandler);
// Create password policy handling strategy
AuthenticationPasswordPolicyHandlingStrategy strategy = new DefaultPasswordPolicyHandlingStrategy<>();
// Handle password policy during authentication
try {
List<MessageDescriptor> messages = strategy.handle(authenticationResponse, policyContext);
for (MessageDescriptor message : messages) {
if (message instanceof PasswordExpiringWarningMessageDescriptor) {
PasswordExpiringWarningMessageDescriptor warning =
(PasswordExpiringWarningMessageDescriptor) message;
System.out.println("Password expires in " + warning.getDaysToExpiration() + " days");
}
}
} catch (AccountPasswordMustChangeException e) {
// Redirect user to password change form
System.out.println("Password must be changed");
} catch (AccountLockedException e) {
// Display account locked message
System.out.println("Account is locked");
}
// Password encoding example
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
String encodedPassword = encoder.encode("plainTextPassword");
boolean matches = encoder.matches("plainTextPassword", encodedPassword);
// Groovy password policy example
String groovyScript = """
import org.apereo.cas.authentication.MessageDescriptor
import org.apereo.cas.authentication.support.password.PasswordExpiringWarningMessageDescriptor
def handle(response, configuration) {
def messages = []
if (response.passwordExpirationDays <= 7) {
messages.add(new PasswordExpiringWarningMessageDescriptor(
"Password expires soon!",
response.passwordExpirationDays))
}
return messages
}
handle(binding.variables.response, binding.variables.configuration)
""";
ExecutableCompiledGroovyScript script = new ExecutableCompiledGroovyScript(groovyScript);
GroovyPasswordPolicyHandlingStrategy groovyStrategy = new GroovyPasswordPolicyHandlingStrategy(script);public class CustomPasswordPolicyHandlingStrategy
implements AuthenticationPasswordPolicyHandlingStrategy<CustomAuthResponse, PasswordPolicyContext> {
@Override
public List<MessageDescriptor> handle(CustomAuthResponse response,
PasswordPolicyContext configuration) throws Throwable {
List<MessageDescriptor> messages = new ArrayList<>();
// Check custom password requirements
if (response.isPasswordWeak()) {
messages.add(new MessageDescriptor() {
public String getCode() { return "password.weak"; }
public String getDefaultMessage() { return "Password is too weak"; }
public Object[] getParams() { return new Object[0]; }
});
}
// Check password age
int passwordAge = response.getPasswordAgeDays();
if (passwordAge > 90) {
messages.add(new PasswordExpiringWarningMessageDescriptor(
"Password is old and should be changed",
90 - passwordAge));
}
// Check account inactivity
if (response.getDaysSinceLastLogin() > 180) {
throw new AccountDisabledException("Account inactive for too long");
}
return messages;
}
}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.
Install with Tessl CLI
npx tessl i tessl/maven-org-apereo-cas--cas-server-core-authentication-api