package org.apereo.cas.authentication.support;

import org.apereo.cas.DefaultMessageDescriptor;
import org.apereo.cas.authentication.AccountPasswordMustChangeException;
import org.apereo.cas.authentication.InvalidLoginTimeException;
import org.apereo.cas.authentication.MessageDescriptor;
import org.apereo.cas.util.DateTimeUtils;
import org.apereo.cas.authentication.AccountDisabledException;
import org.apereo.cas.authentication.InvalidLoginLocationException;

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.PasswordExpirationAccountState;
import org.ldaptive.control.PasswordPolicyControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.AccountLockedException;
import javax.security.auth.login.CredentialExpiredException;
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.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
 */
@RefreshScope
@Component("accountStateHandler")
public class DefaultAccountStateHandler implements AccountStateHandler {
    /** Map of account state error to CAS authentication exception. */
    protected Map<AccountState.Error, LoginException> errorMap;

    /** Logger instance. */
    protected transient Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 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(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 CredentialExpiredException());
    }

    @Override
    public List<MessageDescriptor> handle(final AuthenticationResponse response, final LdapPasswordPolicyConfiguration configuration)
            throws LoginException {

        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. Please change your password at <href=\"{1}\">{1}</a>",
                    ttl,
                    configuration.getPasswordPolicyUrl()));
        }
        if (warning.getLoginsRemaining() > 0) {
            messages.add(new DefaultMessageDescriptor(
                    "password.expiration.loginsRemaining",
                    "You have {0} logins remaining before you MUST change your password.",
                    warning.getLoginsRemaining()));

        }
    }
}

@@ -3608,33 +3608,41 @@
 ET, new 
-CredentialExpired
+AccountPasswordMustChange
 Exceptio

Throw AccountPasswordMustChangeException for CHANGE_AFTER_RESET