package org.apereo.cas.authentication.support; import com.google.common.base.Throwables; import org.apache.shiro.util.ClassUtils; import org.apereo.cas.DefaultMessageDescriptor; 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 org.apereo.cas.authentication.MessageDescriptor; import org.apereo.cas.authentication.support.password.PasswordExpiringWarningMessageDescriptor; import org.apereo.cas.util.DateTimeUtils; import org.ldaptive.LdapAttribute; import org.ldaptive.auth.AccountState; import org.ldaptive.auth.AuthenticationResponse; import org.ldaptive.auth.ext.ActiveDirectoryAccountState; import org.ldaptive.auth.ext.EDirectoryAccountState; import org.ldaptive.auth.ext.FreeIPAAccountState; import org.ldaptive.auth.ext.PasswordExpirationAccountState; import org.ldaptive.control.PasswordPolicyControl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.LinkedCaseInsensitiveMap; import javax.security.auth.login.AccountExpiredException; import javax.security.auth.login.AccountLockedException; import javax.security.auth.login.AccountNotFoundException; import javax.security.auth.login.CredentialExpiredException; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Default account state handler. * * @author Marvin S. Addison * @since 4.0.0 */ public class DefaultAccountStateHandler implements AccountStateHandler { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAccountStateHandler.class); /** * Map of account state error to CAS authentication exception. */ protected Map<AccountState.Error, LoginException> errorMap; private Map<String, Class<LoginException>> attributesToErrorMap = new LinkedCaseInsensitiveMap<>(); /** * Instantiates a new account state handler, that populates * the error map with LDAP error codes and corresponding exceptions. */ public DefaultAccountStateHandler() { this.errorMap = new HashMap<>(); this.errorMap.put(ActiveDirectoryAccountState.Error.ACCOUNT_DISABLED, new AccountDisabledException()); this.errorMap.put(ActiveDirectoryAccountState.Error.ACCOUNT_LOCKED_OUT, new AccountLockedException()); this.errorMap.put(ActiveDirectoryAccountState.Error.INVALID_LOGON_HOURS, new InvalidLoginTimeException()); this.errorMap.put(ActiveDirectoryAccountState.Error.INVALID_WORKSTATION, new InvalidLoginLocationException()); this.errorMap.put(ActiveDirectoryAccountState.Error.PASSWORD_MUST_CHANGE, new AccountPasswordMustChangeException()); this.errorMap.put(ActiveDirectoryAccountState.Error.PASSWORD_EXPIRED, new CredentialExpiredException()); this.errorMap.put(ActiveDirectoryAccountState.Error.ACCOUNT_EXPIRED, new AccountExpiredException()); this.errorMap.put(EDirectoryAccountState.Error.ACCOUNT_EXPIRED, new AccountExpiredException()); this.errorMap.put(EDirectoryAccountState.Error.LOGIN_LOCKOUT, new AccountLockedException()); this.errorMap.put(EDirectoryAccountState.Error.LOGIN_TIME_LIMITED, new InvalidLoginTimeException()); this.errorMap.put(EDirectoryAccountState.Error.PASSWORD_EXPIRED, new CredentialExpiredException()); this.errorMap.put(PasswordExpirationAccountState.Error.PASSWORD_EXPIRED, new CredentialExpiredException()); this.errorMap.put(PasswordPolicyControl.Error.ACCOUNT_LOCKED, new AccountLockedException()); this.errorMap.put(PasswordPolicyControl.Error.PASSWORD_EXPIRED, new CredentialExpiredException()); this.errorMap.put(PasswordPolicyControl.Error.CHANGE_AFTER_RESET, new AccountPasswordMustChangeException()); this.errorMap.put(FreeIPAAccountState.Error.FAILED_AUTHENTICATION, new FailedLoginException()); this.errorMap.put(FreeIPAAccountState.Error.PASSWORD_EXPIRED, new CredentialExpiredException()); this.errorMap.put(FreeIPAAccountState.Error.ACCOUNT_EXPIRED, new AccountExpiredException()); this.errorMap.put(FreeIPAAccountState.Error.MAXIMUM_LOGINS_EXCEEDED, new AccountLockedException()); this.errorMap.put(FreeIPAAccountState.Error.LOGIN_TIME_LIMITED, new InvalidLoginTimeException()); this.errorMap.put(FreeIPAAccountState.Error.LOGIN_LOCKOUT, new AccountLockedException()); this.errorMap.put(FreeIPAAccountState.Error.ACCOUNT_NOT_FOUND, new AccountNotFoundException()); this.errorMap.put(FreeIPAAccountState.Error.CREDENTIAL_NOT_FOUND, new FailedLoginException()); this.errorMap.put(FreeIPAAccountState.Error.ACCOUNT_DISABLED, new AccountDisabledException()); } @Override public List<MessageDescriptor> handle(final AuthenticationResponse response, final LdapPasswordPolicyConfiguration configuration) throws LoginException { if (!this.attributesToErrorMap.isEmpty() && response.getResult()) { LOGGER.debug("Handling policy based on pre-defined attributes"); handlePolicyAttributes(response); } final AccountState state = response.getAccountState(); if (state == null) { LOGGER.debug("Account state not defined. Returning empty list of messages."); return Collections.emptyList(); } final List<MessageDescriptor> messages = new ArrayList<>(); handleError(state.getError(), response, configuration, messages); handleWarning(state.getWarning(), response, configuration, messages); return messages; } /** * Handle an account state error produced by ldaptive account state machinery. * <p> * Override this method to provide custom error handling. * * @param error Account state error. * @param response Ldaptive authentication response. * @param configuration Password policy configuration. * @param messages Container for messages produced by account state error handling. * @throws LoginException On errors that should be communicated as login exceptions. */ protected void handleError( final AccountState.Error error, final AuthenticationResponse response, final LdapPasswordPolicyConfiguration configuration, final List<MessageDescriptor> messages) throws LoginException { LOGGER.debug("Handling error [{}]", error); final LoginException ex = this.errorMap.get(error); if (ex != null) { throw ex; } LOGGER.debug("No LDAP error mapping defined for [{}]", error); } /** * Handle an account state warning produced by ldaptive account state machinery. * <p> * Override this method to provide custom warning message handling. * * @param warning the account state warning messages. * @param response Ldaptive authentication response. * @param configuration Password policy configuration. * @param messages Container for messages produced by account state warning handling. */ protected void handleWarning( final AccountState.Warning warning, final AuthenticationResponse response, final LdapPasswordPolicyConfiguration configuration, final List<MessageDescriptor> messages) { LOGGER.debug("Handling warning [{}]", warning); if (warning == null) { LOGGER.debug("Account state warning not defined"); return; } final ZonedDateTime expDate = DateTimeUtils.zonedDateTimeOf(warning.getExpiration()); final long ttl = ZonedDateTime.now(ZoneOffset.UTC).until(expDate, ChronoUnit.DAYS); LOGGER.debug( "Password expires in [{}] days. Expiration warning threshold is [{}] days.", ttl, configuration.getPasswordWarningNumberOfDays()); if (configuration.isAlwaysDisplayPasswordExpirationWarning() || ttl < configuration.getPasswordWarningNumberOfDays()) { messages.add(new PasswordExpiringWarningMessageDescriptor("Password expires in {0} days.", ttl)); } if (warning.getLoginsRemaining() > 0) { messages.add(new DefaultMessageDescriptor( "password.expiration.loginsRemaining", "You have {0} logins remaining before you MUST change your password.", warning.getLoginsRemaining())); } } public void setAttributesToErrorMap(final Map<String, Class<LoginException>> attributesToErrorMap) { this.attributesToErrorMap = attributesToErrorMap; } /** * Maps boolean attribute values to their corresponding exception. * This handles ad-hoc password policies. * * @param response the authentication response. */ protected void handlePolicyAttributes(final AuthenticationResponse response) { final Collection<LdapAttribute> attrs = response.getLdapEntry().getAttributes(); for (final LdapAttribute attr : attrs) { if (this.attributesToErrorMap.containsKey(attr.getName()) && Boolean.parseBoolean(attr.getStringValue())) { final Class<LoginException> clazz = this.attributesToErrorMap.get(attr.getName()); final LoginException ex = (LoginException) ClassUtils.newInstance(clazz); if (ex != null) { throw Throwables.propagate(ex); } } } } }