nl.strohalm.cyclos.services.access.AccessServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for nl.strohalm.cyclos.services.access.AccessServiceImpl.java

Source

/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
    
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
    
Cyclos is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    
 */
package nl.strohalm.cyclos.services.access;

import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Set;

import nl.strohalm.cyclos.access.AdminMemberPermission;
import nl.strohalm.cyclos.access.AdminSystemPermission;
import nl.strohalm.cyclos.access.BasicPermission;
import nl.strohalm.cyclos.access.BrokerPermission;
import nl.strohalm.cyclos.access.MemberPermission;
import nl.strohalm.cyclos.dao.access.LoginHistoryDAO;
import nl.strohalm.cyclos.dao.access.PasswordHistoryLogDAO;
import nl.strohalm.cyclos.dao.access.PermissionDeniedTraceDAO;
import nl.strohalm.cyclos.dao.access.SessionDAO;
import nl.strohalm.cyclos.dao.access.UserDAO;
import nl.strohalm.cyclos.dao.access.WrongCredentialAttemptsDAO;
import nl.strohalm.cyclos.dao.access.WrongUsernameAttemptsDAO;
import nl.strohalm.cyclos.dao.accounts.cards.CardDAO;
import nl.strohalm.cyclos.dao.members.ElementDAO;
import nl.strohalm.cyclos.entities.Relationship;
import nl.strohalm.cyclos.entities.access.AdminUser;
import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.access.Channel.Credentials;
import nl.strohalm.cyclos.entities.access.LoginHistoryLog;
import nl.strohalm.cyclos.entities.access.MemberUser;
import nl.strohalm.cyclos.entities.access.OperatorUser;
import nl.strohalm.cyclos.entities.access.PasswordHistoryLog;
import nl.strohalm.cyclos.entities.access.PasswordHistoryLog.PasswordType;
import nl.strohalm.cyclos.entities.access.Session;
import nl.strohalm.cyclos.entities.access.SessionQuery;
import nl.strohalm.cyclos.entities.access.User;
import nl.strohalm.cyclos.entities.access.User.TransactionPasswordStatus;
import nl.strohalm.cyclos.entities.accounts.cards.Card;
import nl.strohalm.cyclos.entities.accounts.cards.CardType;
import nl.strohalm.cyclos.entities.alerts.MemberAlert;
import nl.strohalm.cyclos.entities.alerts.SystemAlert;
import nl.strohalm.cyclos.entities.customization.fields.CustomField.Type;
import nl.strohalm.cyclos.entities.customization.fields.CustomFieldValue;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.exceptions.UnexpectedEntityException;
import nl.strohalm.cyclos.entities.groups.AdminGroup;
import nl.strohalm.cyclos.entities.groups.BasicGroupSettings;
import nl.strohalm.cyclos.entities.groups.BasicGroupSettings.PasswordPolicy;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.groups.MemberGroupSettings;
import nl.strohalm.cyclos.entities.groups.OperatorGroup;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.members.Operator;
import nl.strohalm.cyclos.entities.services.ServiceClient;
import nl.strohalm.cyclos.entities.settings.AccessSettings;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.exceptions.MailSendingException;
import nl.strohalm.cyclos.exceptions.PermissionDeniedException;
import nl.strohalm.cyclos.services.InitializingService;
import nl.strohalm.cyclos.services.access.exceptions.AlreadyConnectedException;
import nl.strohalm.cyclos.services.access.exceptions.BlockedCredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.CredentialsAlreadyUsedException;
import nl.strohalm.cyclos.services.access.exceptions.InactiveMemberException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidCardException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidCredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidUserForChannelException;
import nl.strohalm.cyclos.services.access.exceptions.NotConnectedException;
import nl.strohalm.cyclos.services.access.exceptions.SessionAlreadyInUseException;
import nl.strohalm.cyclos.services.access.exceptions.SystemOfflineException;
import nl.strohalm.cyclos.services.access.exceptions.UserNotFoundException;
import nl.strohalm.cyclos.services.accounts.cards.CardServiceLocal;
import nl.strohalm.cyclos.services.alerts.AlertServiceLocal;
import nl.strohalm.cyclos.services.application.ApplicationServiceLocal;
import nl.strohalm.cyclos.services.elements.ElementServiceLocal;
import nl.strohalm.cyclos.services.elements.ResetTransactionPasswordDTO;
import nl.strohalm.cyclos.services.fetch.FetchServiceLocal;
import nl.strohalm.cyclos.services.permissions.PermissionServiceLocal;
import nl.strohalm.cyclos.services.settings.SettingsServiceLocal;
import nl.strohalm.cyclos.utils.CacheCleaner;
import nl.strohalm.cyclos.utils.DataIteratorHelper;
import nl.strohalm.cyclos.utils.EntityHelper;
import nl.strohalm.cyclos.utils.HashHandler;
import nl.strohalm.cyclos.utils.MailHandler;
import nl.strohalm.cyclos.utils.PropertyHelper;
import nl.strohalm.cyclos.utils.RangeConstraint;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.StringHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.TransactionHelper;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.utils.logging.LoggingHandler;
import nl.strohalm.cyclos.utils.logging.TraceLogDTO;
import nl.strohalm.cyclos.utils.notifications.MemberNotificationHandler;
import nl.strohalm.cyclos.utils.query.IteratorList;
import nl.strohalm.cyclos.utils.transaction.CurrentTransactionData;
import nl.strohalm.cyclos.utils.transaction.TransactionEndListener;
import nl.strohalm.cyclos.utils.validation.GeneralValidation;
import nl.strohalm.cyclos.utils.validation.LengthValidation;
import nl.strohalm.cyclos.utils.validation.PropertyValidation;
import nl.strohalm.cyclos.utils.validation.RequiredError;
import nl.strohalm.cyclos.utils.validation.ValidationError;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import nl.strohalm.cyclos.utils.validation.Validator;
import nl.strohalm.cyclos.utils.validation.Validator.Property;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;

/**
 * Implementation class for access services Service.
 * @author rafael
 * @author luis
 */
public class AccessServiceImpl implements AccessServiceLocal, InitializingService {

    private final class LoginPasswordValidation implements PropertyValidation {
        private final Element element;
        private static final long serialVersionUID = -4369049571487478881L;

        private LoginPasswordValidation(final Element element) {
            this.element = element;
        }

        @Override
        public ValidationError validate(final Object object, final Object property, final Object value) {
            final String loginPassword = (String) value;
            final AccessSettings accessSettings = settingsService.getAccessSettings();
            final boolean numeric = accessSettings.isNumericPassword();
            return resolveValidationError(true, numeric, element, object, property, loginPassword);
        }
    }

    private final class PinValidation implements PropertyValidation {
        private final Member member;
        private static final long serialVersionUID = -4369049571487478881L;

        private PinValidation(final Member member) {
            this.member = member;
        }

        @Override
        public ValidationError validate(final Object object, final Object property, final Object value) {
            final String loginPassword = (String) value;
            // pin is always numeric
            return resolveValidationError(false, true, member, object, property, loginPassword);
        }
    }

    private static final String ALLOW_LOGIN_FOR_GROUPS_KEY = "cyclos.allowLoginForGroups";
    private static final String DISALLOW_LOGIN_FOR_GROUPS_KEY = "cyclos.disallowLoginForGroups";

    private static final Relationship FETCH = RelationshipHelper.nested(User.Relationships.ELEMENT,
            Element.Relationships.GROUP);
    private static final Log LOG = LogFactory.getLog(AccessServiceImpl.class);
    private AlertServiceLocal alertService;
    private FetchServiceLocal fetchService;
    private ElementServiceLocal elementService;
    private PermissionServiceLocal permissionService;
    private SettingsServiceLocal settingsService;
    private ElementDAO elementDao;
    private UserDAO userDao;
    private CardDAO cardDao;
    private SessionDAO sessionDao;
    private WrongCredentialAttemptsDAO wrongCredentialAttemptsDao;
    private WrongUsernameAttemptsDAO wrongUsernameAttemptsDao;
    private PermissionDeniedTraceDAO permissionDeniedTraceDao;
    private LoginHistoryDAO loginHistoryDao;
    private PasswordHistoryLogDAO passwordHistoryLogDao;
    private MailHandler mailHandler;
    private LoggingHandler loggingHandler;
    private HashHandler hashHandler;
    private ChannelServiceLocal channelService;
    private ApplicationServiceLocal applicationService;
    private CardServiceLocal cardService;
    private MemberNotificationHandler memberNotificationHandler;
    private Collection<Long> allowLoginForGroups;
    private Collection<Long> disallowLoginForGroups;

    private TransactionHelper transactionHelper;

    @Override
    public void addLoginPasswordValidation(final Element element, final Property property) {
        property.add(new LoginPasswordValidation(element));
    }

    @Override
    public void addPinValidation(final Member member, final Property property) {
        property.add(new PinValidation(member));
    }

    @Override
    public boolean canChangeChannelsAccess(final Member member) {
        return permissionService.permission(member).admin(AdminMemberPermission.ACCESS_CHANGE_CHANNELS_ACCESS)
                .broker(BrokerPermission.MEMBER_ACCESS_CHANGE_CHANNELS_ACCESS)
                .member(MemberPermission.ACCESS_CHANGE_CHANNELS_ACCESS).hasPermission();
    }

    @Override
    public Member changeChannelsAccess(Member member, final Collection<Channel> channels,
            final boolean verifySmsChannel) {
        member = fetchService.fetch(member, Member.Relationships.CHANNELS);
        final Channel smsChannel = channelService.getSmsChannel();
        // When SMS channel is enabled, it is not set directly by this method. So, we need to ensure it remains related to the member after saving
        if (verifySmsChannel && smsChannel != null && member.getChannels().contains(smsChannel)) {
            channels.add(smsChannel);
        }
        member.setChannels(channels);
        return elementDao.update(member);
    }

    @Override
    public void changeCredentials(final MemberUser user, final String newCredentials)
            throws CredentialsAlreadyUsedException {
        ServiceClient client = fetchService.fetch(LoggedUser.serviceClient(),
                RelationshipHelper.nested(ServiceClient.Relationships.CHANNEL, Channel.Relationships.PRINCIPALS));
        switch (client.getChannel().getCredentials()) {
        case LOGIN_PASSWORD:
            changePassword(user, null, newCredentials, false);
            break;
        case PIN:
            changePin(user, newCredentials);
            break;
        }
    }

    @Override
    public User changePassword(final ChangeLoginPasswordDTO params)
            throws InvalidCredentialsException, BlockedCredentialsException, CredentialsAlreadyUsedException {
        validateChangePassword(params);

        User loggedUser = LoggedUser.user();
        boolean myPassword = loggedUser.equals(params.getUser());

        if (myPassword) {
            // We'll only check the old password if it's not expired
            final boolean isExpired = hasPasswordExpired();
            if (!isExpired) {
                // Check the current password
                final Element loggedElement = LoggedUser.element();
                String member = null;
                if (LoggedUser.isOperator()) {
                    final Operator operator = LoggedUser.element();
                    member = operator.getMember().getUsername();
                }
                checkPassword(member, loggedElement.getUsername(), params.getOldPassword(),
                        LoggedUser.remoteAddress());
            }
        }

        final User user = fetchService.fetch(params.getUser(),
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
        return changePassword(user, params.getNewPassword(), params.isForceChange());
    }

    @Override
    public MemberUser changePin(final ChangePinDTO params)
            throws InvalidCredentialsException, BlockedCredentialsException, CredentialsAlreadyUsedException {
        validateChangePin(params);

        User loggedUser = LoggedUser.user();
        boolean myPin = loggedUser.equals(params.getUser());

        if (myPin) {
            // Check whether to enforce the login or transaction password
            final Member loggedMember = (Member) fetchService.fetch(loggedUser.getElement(),
                    Element.Relationships.GROUP);
            final boolean usesTransactionPassword = loggedMember.getMemberGroup().getBasicSettings()
                    .getTransactionPassword().isUsed();

            // If the password (or transaction password) is incorrect an exception is thrown
            if (usesTransactionPassword) {
                checkTransactionPassword(loggedMember.getUser(), params.getCredentials(),
                        LoggedUser.remoteAddress());
            } else {
                checkPassword(loggedMember.getUser(), params.getCredentials(), LoggedUser.remoteAddress());
            }
        }
        final MemberUser user = fetchService.fetch(params.getUser(),
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
        return changePin(user, params.getNewPin());
    }

    @Override
    public MemberUser checkCredentials(Channel channel, MemberUser user, final String credentials,
            final String remoteAddress, final Member relatedMember) {
        if (StringUtils.isEmpty(credentials)) {
            throw new InvalidCredentialsException(channel.getCredentials(), user);
        }
        channel = fetchService.fetch(channel, Channel.Relationships.PRINCIPALS);

        user = fetchService.fetch(user);
        switch (channel.getCredentials()) {
        case DEFAULT:
        case LOGIN_PASSWORD:
            checkPassword(user, credentials, remoteAddress);
            break;
        case PIN:
            checkPin(user, credentials, channel.getInternalName(), relatedMember);
            break;
        case TRANSACTION_PASSWORD:
            checkTransactionPassword(user, credentials, remoteAddress);
            break;
        case CARD_SECURITY_CODE:
            final Card card = cardService.getActiveCard(user.getMember());
            checkCardSecurityCode(card.getCardNumber(), credentials, channel.getInternalName());
            break;
        }
        return user;
    }

    @Override
    public User checkPassword(final String member, final String username, final String plainPassword,
            final String remoteAddress) {
        final User user = loadUser(member, username);
        return checkPassword(user, plainPassword, remoteAddress);
    }

    @Override
    public Session checkSession(final String sessionId) throws NotConnectedException {
        try {
            Session session = sessionDao.load(sessionId, false);
            User user = session.getUser();

            final Long id = session.getId();
            final Calendar newExpiration = getSessionTimeout(user, session.isPosWeb()).add(Calendar.getInstance());

            // After the main transaction ends, we need to update the session
            CurrentTransactionData.addTransactionEndListener(new TransactionEndListener() {
                @Override
                protected void onTransactionEnd(final boolean commit) {
                    updateSessionExpiration(id, newExpiration);
                }
            });
            return session;
        } catch (EntityNotFoundException e) {
            throw new NotConnectedException();
        }
    }

    @Override
    public User checkTransactionPassword(final String transactionPassword)
            throws InvalidCredentialsException, BlockedCredentialsException {
        final User user = LoggedUser.user();
        return checkTransactionPassword(user, transactionPassword, LoggedUser.remoteAddress());
    }

    @Override
    public User disconnect(Session session) throws NotConnectedException {
        try {
            session = fetchService.fetch(session,
                    RelationshipHelper.nested(Session.Relationships.USER, User.Relationships.ELEMENT));
        } catch (EntityNotFoundException e) {
            throw new NotConnectedException();
        }
        sessionDao.delete(session.getId());
        User user = session.getUser();
        if (!isLoggedIn(user)) {
            // If there are no other sessions for that user, set the lastLogin
            user.setLastLogin(Calendar.getInstance());
        }
        return user;
    }

    @Override
    public User disconnect(final User user) {
        int sessions = sessionDao.delete(user);
        if (sessions > 0) {
            // Store the last login
            user.setLastLogin(Calendar.getInstance());
        }
        return fetchService.fetch(user, User.Relationships.ELEMENT);
    }

    @Override
    public void disconnectAllButLogged() {
        final User loggedUser = LoggedUser.user();
        IteratorList<User> iterator = sessionDao.listLoggedUsers();
        try {
            CacheCleaner cacheCleaner = new CacheCleaner(fetchService);
            for (User user : iterator) {
                if (!loggedUser.equals(user)) {
                    disconnect(user);
                    cacheCleaner.clearCache();
                }
            }
        } finally {
            DataIteratorHelper.close(iterator);
        }
    }

    @Override
    public String generatePassword(Group group) {
        if (group instanceof OperatorGroup) {
            group = fetchService.fetch(group,
                    RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
        }
        final Integer min = group.getBasicSettings().getPasswordLength().getMin();
        final boolean onlyNumbers = settingsService.getAccessSettings().isNumericPassword();
        return RandomStringUtils.random(min == null ? 4 : min, !onlyNumbers, true).toLowerCase();
    }

    @Override
    public String generateTransactionPassword() {

        User user = LoggedUser.user();

        // Load operators member and group of operators member
        if (user instanceof OperatorUser) {
            Element element = user.getElement();
            element = fetchService.fetch(element,
                    RelationshipHelper.nested(Operator.Relationships.MEMBER, Element.Relationships.GROUP));
        }

        // If the transaction password is not used for the logged user, return null
        if (!requestTransactionPassword(user)) {
            throw new UnexpectedEntityException();
        }

        // If there is already a password, return null
        final String current = user.getTransactionPassword();
        if (current != null) {
            return null;
        }

        // Get the chars
        final AccessSettings accessSettings = settingsService.getAccessSettings();
        final String chars = accessSettings.getTransactionPasswordChars();

        // Get the password length
        Group group = user.getElement().getGroup();
        if (group instanceof OperatorGroup) {
            group = fetchService.fetch(group,
                    RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
        }
        final BasicGroupSettings basicSettings = group.getBasicSettings();
        final int length = basicSettings.getTransactionPasswordLength();

        // Generate a new one, and store it
        final StringBuilder buffer = new StringBuilder(length);
        final Random rnd = new Random();
        for (int i = 0; i < length; i++) {
            buffer.append(chars.charAt(rnd.nextInt(chars.length())));
        }
        final String transactionPassword = buffer.toString();
        user.setTransactionPassword(hashHandler.hash(user.getSalt(), transactionPassword));
        user.setTransactionPasswordStatus(TransactionPasswordStatus.ACTIVE);
        user = userDao.update(user);
        return transactionPassword;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Collection<Channel> getChannelsEnabledForMember(Member member) {
        member = fetchService.fetch(member, Element.Relationships.GROUP);
        return CollectionUtils.retainAll(member.getChannels(), member.getMemberGroup().getChannels());
    }

    @Override
    public User getLoggedUser(final String sessionId) throws NotConnectedException {
        try {
            Session session = sessionDao.load(sessionId, false);
            return session.getUser();
        } catch (EntityNotFoundException e) {
            throw new NotConnectedException();
        }
    }

    @Override
    public boolean hasPasswordExpired() {
        Group group = LoggedUser.group();
        if (group instanceof OperatorGroup) {
            group = fetchService.fetch(group,
                    RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
        }
        final TimePeriod exp = group.getBasicSettings().getPasswordExpiresAfter();
        final Calendar passwordDate = LoggedUser.user().getPasswordDate();
        if (passwordDate == null) {
            return true;
        }
        if (exp != null && exp.getNumber() > 0 && passwordDate != null) {
            final Calendar expiresAt = exp.remove(Calendar.getInstance());
            return expiresAt.after(passwordDate);
        }
        return false;
    }

    @Override
    public void initializeService() {
        purgeTraces(Calendar.getInstance());
        purgeExpiredSessions();
    }

    @Override
    public boolean isCardSecurityCodeBlocked(Card card) {
        card = fetchService.reload(card);
        return isBlocked(card.getCardSecurityCodeBlockedUntil());
    }

    @Override
    public boolean isChannelAllowedToBeEnabledForMember(final Channel channel, Member member) {
        member = fetchService.fetch(member, Element.Relationships.GROUP, Member.Relationships.CHANNELS);

        switch (channel.getCredentials()) {
        case TRANSACTION_PASSWORD:
            // Check if the member's group uses transaction password.
            if (!member.getMemberGroup().getBasicSettings().getTransactionPassword().isUsed()) {
                LOG.warn("The member's group doesn't use transaction password,  member: " + member);
                return false;
            }
            break;
        case CARD_SECURITY_CODE:
            // Check if the member's group has a Card Type
            if (member.getMemberGroup().getCardType() == null) {
                LOG.warn("The member's group doesn't have a card type,  member: " + member);
                return false;
            }
            break;
        }

        // If the group doesn't have access to the channel, the member can't access it too
        if (!isChannelEnabledForGroup(channel, member.getMemberGroup())) {
            return false;
        }

        return true;
    }

    @Override
    public boolean isChannelEnabledForMember(final Channel channel, Member member) {
        // Check the member access customization too
        member = fetchService.fetch(member, Member.Relationships.CHANNELS);
        return isChannelAllowedToBeEnabledForMember(channel, member)
                && (channel.getInternalName().equals(Channel.WEB) || member.getChannels().contains(channel));
    }

    @Override
    public boolean isChannelEnabledForMember(final String channelInternalName, final Member member) {
        final Channel channel = channelService.loadByInternalName(channelInternalName);
        return isChannelEnabledForMember(channel, member);
    }

    @Override
    public boolean isLoggedIn(final User user) {
        return sessionDao.isLoggedIn(user);
    }

    @Override
    public boolean isLoginBlocked(User user) {
        user = fetchService.reload(user);
        return isBlocked(user.getPasswordBlockedUntil());
    }

    @Override
    public boolean isObviousCredential(Element element, final String credential) {

        // Ensure not to fetch the element for new records
        if (element.isPersistent()) {
            element = fetchService.fetch(element, Element.Relationships.USER, Member.Relationships.CUSTOM_VALUES);
        }

        // If the credential is equals to the username, it is obvious
        if (credential.equalsIgnoreCase(element.getUsername())) {
            return true;
        }

        // If the credential is equals to any word of the full name, it is obvious
        final String[] nameParts = StringUtils.split(element.getName(), " .,/-\\");
        for (final String part : nameParts) {
            if (credential.equalsIgnoreCase(part)) {
                return true;
            }
        }

        // If the credential is equals to the user part of the e-mail, it is obvious
        final String email = element.getEmail();
        if (StringUtils.isNotEmpty(email) && email.contains("@")) {
            final String mailUser = StringUtils.split(email, "@", 1)[0];
            if (credential.equalsIgnoreCase(mailUser)) {
                return true;
            }
        }

        // If the credential matches custom field values, it is obvious
        final Collection<CustomFieldValue> customValues = PropertyHelper.get(element, "customValues");
        final LocalSettings localSettings = settingsService.getLocalSettings();
        for (final CustomFieldValue fieldValue : customValues) {
            final Type type = fieldValue.getField().getType();
            final String stringValue = fieldValue.getStringValue();
            if (StringUtils.isEmpty(stringValue)) {
                continue;
            }
            switch (type) {
            case DATE:
                // The credential cannot be the same as a date
                final String unmasked = StringHelper.removeMask(localSettings.getDatePattern().getPattern(),
                        stringValue);
                if (credential.equals(unmasked)) {
                    return true;
                }
                break;
            case INTEGER:
                // The credential cannot be equal to an integer
                if (credential.equals(stringValue)) {
                    return true;
                }
                break;
            case STRING:
                // The credential cannot be contained in a string
                if (stringValue.contains(credential)) {
                    return true;
                }
                break;
            }
        }

        // When all chars have the same distance, it's obvious (things like 1234, 7654, abcdef and so on...)
        if (credential.length() > 1) {
            final Set<Integer> diffs = new HashSet<Integer>();
            for (int i = 1, len = credential.length(); i < len; i++) {
                final char current = credential.charAt(i);
                final char previous = credential.charAt(i - 1);
                diffs.add(current - previous);
                if (diffs.size() > 1) {
                    // More than 1 difference means it's not obvious
                    break;
                }
            }
            if (diffs.size() == 1) {
                return true;
            }
        }

        return false;
    }

    @Override
    public boolean isPinBlocked(MemberUser user) {
        user = fetchService.reload(user);
        return isBlocked(user.getPinBlockedUntil());
    }

    @Override
    public User login(User user, final String plainCredentials, final String channelName, final boolean isPosWeb,
            final String remoteAddress, final String sessionId)
            throws UserNotFoundException, InvalidCredentialsException, BlockedCredentialsException,
            SessionAlreadyInUseException, AlreadyConnectedException {
        if (user == null) {
            throw new UnexpectedEntityException();
        }
        user = fetchService.fetch(user,
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));

        // When the system is offline, only allow login users with manage online state permission
        if (!applicationService.isOnline()) {
            if (!permissionService.hasPermission(user.getElement().getGroup(),
                    AdminSystemPermission.TASKS_ONLINE_STATE)) {
                throw new SystemOfflineException();
            }
        }

        // Validate the given session id
        try {
            Session session = sessionDao.load(sessionId, false);
            if (session.getUser().equals(user) && remoteAddress.equals(session.getRemoteAddress())
                    && isPosWeb == session.isPosWeb()) {
                // If the same logged user and remote address are under the same session id, return ok
                return session.getUser();
            }
            // The session is in use by another user / remote address
            throw new SessionAlreadyInUseException(user.getUsername());
        } catch (EntityNotFoundException e) {
            // Ok, no session with the given id
        }

        final Channel channel = channelService.loadByInternalName(channelName);

        final boolean isMainWebChannel = Channel.WEB.equals(channel.getInternalName());
        if (user instanceof MemberUser) {
            // For members, check the channel's credentials
            checkCredentials(channel, (MemberUser) user, plainCredentials, remoteAddress, null);
            // Also, ensure the channel is enabled for the member (if not the main web channel)
            if (!isMainWebChannel && !isChannelEnabledForMember(channelName, ((MemberUser) user).getMember())) {
                throw new InvalidUserForChannelException(user.getUsername());
            }
        } else {
            // For admins or operators, always check the login
            if (channel != null && !isMainWebChannel) {
                // Also, they can only login in the main web channel
                throw new PermissionDeniedException();
            }
            checkPassword(user, plainCredentials, remoteAddress);
        }

        // Check if already connected
        AccessSettings accessSettings = settingsService.getAccessSettings();
        if (!accessSettings.isAllowMultipleLogins() && isLoggedIn(user)) {
            throw new AlreadyConnectedException();
        }

        // Initialize the permission denied counter
        permissionDeniedTraceDao.clear(user);

        Calendar now = Calendar.getInstance();

        // Store the session
        Session session = new Session();
        session.setCreationDate(now);
        session.setExpirationDate(getSessionTimeout(user, isPosWeb).add(now));
        session.setUser(user);
        session.setIdentifier(sessionId);
        session.setRemoteAddress(remoteAddress);
        session.setPosWeb(isPosWeb);
        sessionDao.insert(session);

        // Save an entry in the login history
        final LoginHistoryLog loginHistoryLog = new LoginHistoryLog();
        loginHistoryLog.setUser(user);
        loginHistoryLog.setDate(now);
        loginHistoryLog.setRemoteAddress(remoteAddress);
        loginHistoryDao.insert(loginHistoryLog);

        // Generate log
        TraceLogDTO logParams = new TraceLogDTO();
        logParams.setUser(user);
        logParams.setSessionId(sessionId);
        logParams.setRemoteAddress(remoteAddress);
        loggingHandler.traceLogin(logParams);

        // Initialize the login data
        LoggedUser.init(user);

        return user;
    }

    @Override
    public User logout(final String sessionId) {
        try {
            Session session = sessionDao.load(sessionId, true);
            sessionDao.delete(session.getId());
            User user = session.getUser();

            user.setLastLogin(Calendar.getInstance());

            // Trace to the log file
            TraceLogDTO logParams = new TraceLogDTO();
            logParams.setUser(user);
            logParams.setSessionId(sessionId);
            logParams.setRemoteAddress(session.getRemoteAddress());
            loggingHandler.traceLogout(logParams);

            // Clean up the LoggedUser
            if (LoggedUser.hasUser() && user.equals(LoggedUser.user())) {
                LoggedUser.cleanup();
            }

            return user;
        } catch (EntityNotFoundException e) {
            return null;
        }
    }

    @Override
    public boolean notifyPermissionDeniedException() {
        if (!LoggedUser.hasUser()) {
            return false;
        }

        return transactionHelper.runInNewTransaction(new TransactionCallback<Boolean>() {
            @Override
            public Boolean doInTransaction(final TransactionStatus status) {
                final User user = fetchService.fetch(LoggedUser.user(),
                        RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));

                // Already blocked
                if (isBlocked(user.getPasswordBlockedUntil())) {
                    return true;
                }

                // Check if maxTries has been reached
                final BasicGroupSettings basicSettings = user.getElement().getGroup().getBasicSettings();
                final int maxTries = basicSettings.getMaxPasswordWrongTries();
                if (maxTries > 0) {
                    permissionDeniedTraceDao.record(user);
                    int tries = permissionDeniedTraceDao.count(wrongAttemptsLimit(), user);
                    if (tries == maxTries) {
                        // Send an alert
                        if (user instanceof AdminUser) {
                            alertService.create(SystemAlert.Alerts.ADMIN_LOGIN_BLOCKED_BY_PERMISSION_DENIEDS,
                                    user.getUsername(), tries, LoggedUser.remoteAddress());
                        } else if (user instanceof MemberUser) {
                            alertService.create(((MemberUser) user).getMember(),
                                    MemberAlert.Alerts.LOGIN_BLOCKED_BY_PERMISSION_DENIEDS, tries,
                                    LoggedUser.remoteAddress());
                        }
                        // Block the user and clear any previous traces
                        final Calendar mayLoginAt = basicSettings.getDeactivationAfterMaxPasswordTries()
                                .add(Calendar.getInstance());
                        user.setPasswordBlockedUntil(mayLoginAt);
                        permissionDeniedTraceDao.clear(user);
                        return true;
                    }
                }
                return false;
            }
        });
    }

    @Override
    public void purgeExpiredSessions() {
        sessionDao.purgeExpired();
    }

    @Override
    public void purgeTraces(final Calendar time) {
        Calendar limit = (Calendar) time.clone();
        limit.add(Calendar.DAY_OF_MONTH, -1);
        wrongCredentialAttemptsDao.clear(limit);
        wrongUsernameAttemptsDao.clear(limit);
        permissionDeniedTraceDao.clear(limit);
    }

    @Override
    public User reenableLogin(User user) {
        user = fetchService.fetch(user);
        user.setPasswordBlockedUntil(null);
        wrongCredentialAttemptsDao.clear(user, Credentials.LOGIN_PASSWORD);
        return user;
    }

    @Override
    public MemberUser resetPassword(MemberUser user) throws MailSendingException {

        // Check if password will be sent by mail
        user = fetchService.fetch(user, FETCH);
        final MemberGroup group = user.getMember().getMemberGroup();
        final boolean sendPasswordByEmail = group.getMemberSettings().isSendPasswordByEmail();

        String newPassword = null;
        if (sendPasswordByEmail) {
            // If send by mail, generate a new password
            newPassword = generatePassword(group);
        }

        // Update the user
        user.setPassword(hashHandler.hash(user.getSalt(), newPassword));
        user.setPasswordDate(null);
        userDao.update(user);

        if (sendPasswordByEmail) {
            // Send the password by mail
            mailHandler.sendResetPassword(user.getMember(), newPassword);
        }
        return user;
    }

    @Override
    public User resetTransactionPassword(final ResetTransactionPasswordDTO dto) {
        User user = dto.getUser();
        user.setTransactionPassword(null);
        user.setTransactionPasswordStatus(
                dto.isAllowGeneration() ? TransactionPasswordStatus.PENDING : TransactionPasswordStatus.BLOCKED);
        return userDao.update(user);
    }

    @Override
    public List<Session> searchSessions(final SessionQuery query) {
        return sessionDao.search(query);
    }

    public void setAlertServiceLocal(final AlertServiceLocal alertService) {
        this.alertService = alertService;
    }

    public void setApplicationServiceLocal(final ApplicationServiceLocal applicationService) {
        this.applicationService = applicationService;
    }

    public void setCardDao(final CardDAO cardDao) {
        this.cardDao = cardDao;
    }

    public void setCardServiceLocal(final CardServiceLocal cardService) {
        this.cardService = cardService;
    }

    public void setChannelServiceLocal(final ChannelServiceLocal channelService) {
        this.channelService = channelService;
    }

    public void setCyclosProperties(final Properties cyclosProperties) {
        try {
            allowLoginForGroups = EntityHelper.parseIds(cyclosProperties.getProperty(ALLOW_LOGIN_FOR_GROUPS_KEY));
        } catch (Exception e) {
            throw new IllegalStateException(
                    "Invalid value for " + ALLOW_LOGIN_FOR_GROUPS_KEY + " in cyclos.properties", e);
        }
        try {
            disallowLoginForGroups = EntityHelper
                    .parseIds(cyclosProperties.getProperty(DISALLOW_LOGIN_FOR_GROUPS_KEY));
        } catch (Exception e) {
            throw new IllegalStateException(
                    "Invalid value for " + DISALLOW_LOGIN_FOR_GROUPS_KEY + " in cyclos.properties", e);
        }
    }

    public void setElementDao(final ElementDAO elementDao) {
        this.elementDao = elementDao;
    }

    public void setElementServiceLocal(final ElementServiceLocal elementService) {
        this.elementService = elementService;
    }

    public void setFetchServiceLocal(final FetchServiceLocal fetchService) {
        this.fetchService = fetchService;
    }

    public void setHashHandler(final HashHandler hashHandler) {
        this.hashHandler = hashHandler;
    }

    public void setLoggingHandler(final LoggingHandler loggingHandler) {
        this.loggingHandler = loggingHandler;
    }

    public void setLoginHistoryDao(final LoginHistoryDAO loginHistoryDao) {
        this.loginHistoryDao = loginHistoryDao;
    }

    public void setMailHandler(final MailHandler mailHandler) {
        this.mailHandler = mailHandler;
    }

    public void setMemberNotificationHandler(final MemberNotificationHandler memberNotificationHandler) {
        this.memberNotificationHandler = memberNotificationHandler;
    }

    public void setPasswordHistoryLogDao(final PasswordHistoryLogDAO passwordHistoryLogDao) {
        this.passwordHistoryLogDao = passwordHistoryLogDao;
    }

    public void setPermissionDeniedTraceDao(final PermissionDeniedTraceDAO permissionDeniedTraceDao) {
        this.permissionDeniedTraceDao = permissionDeniedTraceDao;
    }

    public void setPermissionServiceLocal(final PermissionServiceLocal permissionService) {
        this.permissionService = permissionService;
    }

    public void setSessionDao(final SessionDAO sessionDao) {
        this.sessionDao = sessionDao;
    }

    public void setSettingsServiceLocal(final SettingsServiceLocal settingsService) {
        this.settingsService = settingsService;
    }

    public void setTransactionHelper(final TransactionHelper transactionHelper) {
        this.transactionHelper = transactionHelper;
    }

    public void setUserDao(final UserDAO userDao) {
        this.userDao = userDao;
    }

    public void setWrongCredentialAttemptsDao(final WrongCredentialAttemptsDAO wrongCredentialAttemptsDao) {
        this.wrongCredentialAttemptsDao = wrongCredentialAttemptsDao;
    }

    public void setWrongUsernameAttemptsDao(final WrongUsernameAttemptsDAO wrongUsernameAttemptsDao) {
        this.wrongUsernameAttemptsDao = wrongUsernameAttemptsDao;
    }

    @Override
    public Card unblockCardSecurityCode(final BigInteger cardNumber) {
        Card card = cardDao.loadByNumber(cardNumber);
        card.setCardSecurityCodeBlockedUntil(null);
        wrongCredentialAttemptsDao.clear(card);
        return card;
    }

    @Override
    public MemberUser unblockPin(MemberUser user) {
        user = fetchService.fetch(user);
        user.setPinBlockedUntil(null);
        wrongCredentialAttemptsDao.clear(user, Credentials.PIN);
        return user;
    }

    @Override
    public void validateChangePassword(final ChangeLoginPasswordDTO params) throws ValidationException {
        final Validator validator = new Validator("changePassword");
        validator.property("user").required();
        if (LoggedUser.hasUser() && LoggedUser.user().equals(params.getUser())) {
            // The old password is required if it is not expired
            if (!hasPasswordExpired()) {
                validator.property("oldPassword").required();
            }
        }
        final User user = fetchService.fetch(params.getUser(),
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
        if (user != null) {
            validator.property("newPassword").required().add(new LoginPasswordValidation(user.getElement()));
            validator.property("newPasswordConfirmation").required();
        }
        validator.general(new GeneralValidation() {
            private static final long serialVersionUID = -4110708889147050967L;

            @Override
            public ValidationError validate(final Object object) {
                final ChangeLoginPasswordDTO params = (ChangeLoginPasswordDTO) object;
                final String newPassword = params.getNewPassword();
                final String newPasswordConfirmation = params.getNewPasswordConfirmation();
                if (StringUtils.isNotEmpty(newPassword) && StringUtils.isNotEmpty(newPasswordConfirmation)
                        && !newPassword.equals(newPasswordConfirmation)) {
                    return new ValidationError("errors.passwords");
                }
                return null;
            }
        });
        validator.validate(params);
    }

    @Override
    public void validateChangePin(final ChangePinDTO params) {
        if (params.getUser() == null) {
            throw new ValidationException("user", "changePin.user", new RequiredError());
        }
        boolean myPin = LoggedUser.user().equals(params.getUser());
        final Validator validator = new Validator("changePin");
        if (myPin) {
            final MemberGroup group = LoggedUser.group();
            final boolean isTP = group.getBasicSettings().getTransactionPassword().isUsed();
            final String key = isTP ? "channel.credentials.TRANSACTION_PASSWORD"
                    : "channel.credentials.LOGIN_PASSWORD";
            validator.property("credentials").key(key).required();
        } else {
            validator.property("user").required();
        }
        final MemberUser user = fetchService.fetch(params.getUser(), User.Relationships.ELEMENT);
        if (user != null) {
            validator.property("newPin").required().add(new PinValidation(user.getMember()));
            validator.property("newPinConfirmation").required();
        }
        validator.general(new GeneralValidation() {
            private static final long serialVersionUID = -4110708889147050967L;

            @Override
            public ValidationError validate(final Object object) {
                final ChangePinDTO params = (ChangePinDTO) object;
                final String newPin = params.getNewPin();
                final String newPinConfirmation = params.getNewPinConfirmation();
                if (StringUtils.isNotEmpty(newPin) && StringUtils.isNotEmpty(newPinConfirmation)
                        && !newPin.equals(newPinConfirmation)) {
                    return new ValidationError("changePin.error.pinsAreNotEqual");
                }
                return null;
            }
        });
        validator.validate(params);
    }

    public ValidationError validateLoginPassword(final Element element, final String loginPassword) {
        return new LoginPasswordValidation(element).validate(element.getUser(), "password", loginPassword);
    }

    @Override
    public User verifyLogin(final String member, final String username, final String remoteAddress)
            throws UserNotFoundException, InactiveMemberException, PermissionDeniedException {
        try {
            final User user = loadUser(member, username);

            // Check if this user is allowed to login on this Cyclos instance
            Long groupId;
            if (user instanceof OperatorUser) {
                // For operators, check for the member's group id
                groupId = ((OperatorUser) user).getOperator().getMember().getGroup().getId();
            } else {
                groupId = user.getElement().getGroup().getId();
            }

            // Check for the group white-list
            if (!allowLoginForGroups.isEmpty() && !allowLoginForGroups.contains(groupId)) {
                // Not allowed to login - respond just like an invalid user
                throw new UserNotFoundException(username);
            }

            // Check for the group black-list
            if (!disallowLoginForGroups.isEmpty() && disallowLoginForGroups.contains(groupId)) {
                // Not allowed to login - respond just like an invalid user
                throw new UserNotFoundException(username);
            }

            // Check if there's an active password
            if (StringUtils.isEmpty(user.getPassword())) {
                throw new InactiveMemberException(username);
            }

            // Check if the member has permission to login
            if (!permissionService.hasPermission(user.getElement().getGroup(), BasicPermission.BASIC_LOGIN)) {
                throw new PermissionDeniedException();
            }

            // Username exists: remove the records for the given remote address
            wrongUsernameAttemptsDao.clear(remoteAddress);

            return user;
        } catch (final EntityNotFoundException e) {
            // Record the incorrect attempt
            wrongUsernameAttemptsDao.record(remoteAddress);

            // Check if an alert will be sent
            final int maxTries = settingsService.getAlertSettings().getAmountIncorrectLogin();
            if (maxTries > 0) {
                int wrongTries = wrongUsernameAttemptsDao.count(wrongAttemptsLimit(), remoteAddress);
                if (wrongTries == maxTries) {
                    alertService.create(SystemAlert.Alerts.MAX_INCORRECT_LOGIN_ATTEMPTS, wrongTries, remoteAddress);
                    wrongUsernameAttemptsDao.clear(remoteAddress);
                }
            }
            throw new UserNotFoundException(username);
        }
    }

    @Override
    public Calendar wrongAttemptsLimit() {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_MONTH, -1);
        return calendar;
    }

    private <U extends User> U changePassword(U user, final String oldPassword, final String plainNewPassword,
            final boolean forceChange) {
        user = fetchService.reload(user,
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));

        // Before changing, ensure that the new password is valid
        final ValidationError validationResult = new LoginPasswordValidation(user.getElement()).validate(user,
                "password", plainNewPassword);
        if (validationResult != null) {
            throw new ValidationException("password", "channel.credentials.LOGIN_PASSWORD", validationResult);
        }

        final String currentPassword = user.getPassword();
        final String hashedOldPassword = hashHandler.hash(user.getSalt(), oldPassword);
        if (StringUtils.isNotEmpty(oldPassword) && !hashedOldPassword.equalsIgnoreCase(currentPassword)) {
            throw new InvalidCredentialsException(Credentials.LOGIN_PASSWORD, user);
        }

        final String newPassword = hashHandler.hash(user.getSalt(), plainNewPassword);

        final PasswordPolicy passwordPolicy = user.getElement().getGroup().getBasicSettings().getPasswordPolicy();
        if (passwordPolicy != PasswordPolicy.NONE) {
            // Check if it was already in use when there's a password policy
            if (StringUtils.trimToEmpty(currentPassword).equalsIgnoreCase(newPassword)
                    || passwordHistoryLogDao.wasAlreadyUsed(user, PasswordType.LOGIN, newPassword)) {
                throw new CredentialsAlreadyUsedException(Credentials.LOGIN_PASSWORD, user);
            }
        }

        // Ensure the login password is not equals to the pin or transaction password
        if (newPassword.equalsIgnoreCase(user.getTransactionPassword())
                || (user instanceof MemberUser && newPassword.equalsIgnoreCase(((MemberUser) user).getPin()))) {
            throw new ValidationException("changePassword.error.sameAsTransactionPasswordOrPin");
        }

        user.setPassword(newPassword);
        user.setPasswordDate(forceChange ? null : Calendar.getInstance());
        // Ensure that the returning user will have the ELEMENT and GROUP fetched
        user = fetchService.fetch(userDao.update(user), FETCH);
        if (user instanceof OperatorUser) {
            // When an operator, also ensure that the member will have the ELEMENT and GROUP fetched
            final Operator operator = ((OperatorUser) user).getOperator();
            final Member member = fetchService.fetch(operator.getMember(), Element.Relationships.USER,
                    Element.Relationships.GROUP);
            operator.setMember(member);
        }

        // Log the password history
        if (StringUtils.isNotEmpty(currentPassword)) {
            final PasswordHistoryLog log = new PasswordHistoryLog();
            log.setDate(Calendar.getInstance());
            log.setUser(user);
            log.setType(PasswordType.LOGIN);
            log.setPassword(currentPassword);
            passwordHistoryLogDao.insert(log);
        }

        return user;
    }

    private User changePassword(User user, final String plainPassword, final boolean forceChange) {
        // Before changing, ensure that the new password is valid
        final ValidationError validationResult = new LoginPasswordValidation(user.getElement()).validate(user,
                "password", plainPassword);
        if (validationResult != null) {
            throw new ValidationException("password", "channel.credentials.LOGIN_PASSWORD", validationResult);
        }

        final String password = hashHandler.hash(user.getSalt(), plainPassword);
        user = fetchService.fetch(user,
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
        final String currentPassword = user.getPassword();

        final PasswordPolicy passwordPolicy = user.getElement().getGroup().getBasicSettings().getPasswordPolicy();
        if (passwordPolicy != null && passwordPolicy != PasswordPolicy.NONE) {
            // Check if it was already in use when there's a password policy
            if (StringUtils.trimToEmpty(currentPassword).equalsIgnoreCase(password)
                    || passwordHistoryLogDao.wasAlreadyUsed(user, PasswordType.LOGIN, password)) {
                throw new CredentialsAlreadyUsedException(Credentials.LOGIN_PASSWORD, user);
            }
        }

        // Ensure the login password is not equals to the pin or transaction password
        final String pin = user instanceof MemberUser ? ((MemberUser) user).getPin() : null;
        if (password.equalsIgnoreCase(pin) || password.equalsIgnoreCase(user.getTransactionPassword())) {
            throw new ValidationException("changePassword.error.sameAsTransactionPasswordOrPin");
        }

        // Set the new password
        user.setPassword(password);
        if (forceChange) {
            user.setPasswordDate(null);
        } else {
            user.setPasswordDate(Calendar.getInstance());
        }

        // Log the password history
        if (StringUtils.isNotEmpty(currentPassword)) {
            final PasswordHistoryLog log = new PasswordHistoryLog();
            log.setDate(Calendar.getInstance());
            log.setUser(user);
            log.setType(PasswordType.LOGIN);
            log.setPassword(currentPassword);
            passwordHistoryLogDao.insert(log);
        }
        user = userDao.update(user);
        return user;
    }

    private MemberUser changePin(MemberUser user, final String plainPin) {

        // Before changing, ensure that the new pin is valid
        final ValidationError validationResult = new PinValidation(user.getMember()).validate(user, "pin",
                plainPin);
        if (validationResult != null) {
            throw new ValidationException("pin", "channel.credentials.PIN", validationResult);
        }

        final String pin = hashHandler.hash(user.getSalt(), plainPin);
        user = fetchService.fetch(user,
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));
        final String currentPin = user.getPin();

        final PasswordPolicy passwordPolicy = user.getElement().getGroup().getBasicSettings().getPasswordPolicy();
        if (passwordPolicy != null && passwordPolicy != PasswordPolicy.NONE) {
            // Check if it was already in use when there's a password policy
            if (StringUtils.trimToEmpty(currentPin).equalsIgnoreCase(pin)
                    || passwordHistoryLogDao.wasAlreadyUsed(user, PasswordType.PIN, pin)) {
                throw new CredentialsAlreadyUsedException(Credentials.PIN, user);
            }
        }

        // Ensure the login password is not equals to the pin or transaction password
        if (pin.equalsIgnoreCase(user.getPassword()) || pin.equalsIgnoreCase(user.getTransactionPassword())) {
            throw new ValidationException("changePin.error.sameAsLoginOrTransactionPassword");
        }

        // Set the new pin
        user.setPin(pin);

        // Log the pin history
        if (StringUtils.isNotEmpty(currentPin)) {
            final PasswordHistoryLog log = new PasswordHistoryLog();
            log.setDate(Calendar.getInstance());
            log.setUser(user);
            log.setType(PasswordType.PIN);
            log.setPassword(currentPin);
            passwordHistoryLogDao.insert(log);
        }

        return user;
    }

    private Card checkCardSecurityCode(final BigInteger cardNumber, String securityCode, final String channel) {
        Card card;
        try {
            card = cardService.loadByNumber(cardNumber,
                    RelationshipHelper.nested(Card.Relationships.OWNER, Element.Relationships.USER),
                    Card.Relationships.CARD_TYPE);
            if (card.getStatus() != Card.Status.ACTIVE) {
                // Ensure the card is active
                throw new Exception();
            }
        } catch (final Exception e) {
            throw new InvalidCardException();
        }
        User user = card.getOwner().getUser();

        // Check if not already blocked
        if (isBlocked(card.getCardSecurityCodeBlockedUntil())) {
            throw new BlockedCredentialsException(Credentials.CARD_SECURITY_CODE, user);
        }

        // If the securiry code is manual, it is stored hashed, so we must hash the input as well
        if (!card.getCardType().isShowCardSecurityCode()) {
            securityCode = hashHandler.hash(user.getSalt(), securityCode);
        }

        final String storedCode = card.getCardSecurityCode();
        if (StringUtils.isEmpty(storedCode) || !securityCode.equals(storedCode)) {
            final CardType cardType = card.getCardType();
            final int maxTries = cardType.getMaxSecurityCodeTries();
            wrongCredentialAttemptsDao.record(card);
            int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), card);
            if (wrongAttempts == maxTries) {
                // Notify blocking
                memberNotificationHandler.blockedCredentialsNotification(user, Credentials.CARD_SECURITY_CODE);

                // Mark the card as blocked, and remove previous attempts
                final TimePeriod blockTime = cardType.getSecurityCodeBlockTime();
                card.setCardSecurityCodeBlockedUntil(blockTime.add(Calendar.getInstance()));
                wrongCredentialAttemptsDao.clear(card);

                throw new BlockedCredentialsException(Credentials.CARD_SECURITY_CODE, user);
            }
            throw new InvalidCredentialsException(Credentials.CARD_SECURITY_CODE, user);
        }

        // Everything ok - remove any previous wrong attempts
        wrongCredentialAttemptsDao.clear(card);
        return card;
    }

    private User checkPassword(User user, final String plainPassword, final String remoteAddress) {
        user = fetchService.fetch(user,
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));

        // Check if not already blocked
        if (isBlocked(user.getPasswordBlockedUntil())) {
            throw new BlockedCredentialsException(Credentials.LOGIN_PASSWORD, user);
        }

        // Check the password
        final String password = hashHandler.hash(user.getSalt(), plainPassword);
        final String userPassword = user.getPassword();
        if (userPassword == null || !userPassword.equalsIgnoreCase(StringUtils.trimToNull(password))) {
            final BasicGroupSettings basicSettings = user.getElement().getGroup().getBasicSettings();

            // Check if maxTries has been reached
            final int maxTries = basicSettings.getMaxPasswordWrongTries();
            wrongCredentialAttemptsDao.record(user, Credentials.LOGIN_PASSWORD);
            int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), user,
                    Credentials.LOGIN_PASSWORD);
            if (wrongAttempts == maxTries) {
                TimePeriod blockTime = basicSettings.getDeactivationAfterMaxPasswordTries();
                // Create the alert
                if (user instanceof AdminUser) {
                    alertService.create(SystemAlert.Alerts.ADMIN_LOGIN_BLOCKED_BY_TRIES, user.getUsername(),
                            wrongAttempts, remoteAddress);
                } else if (user instanceof MemberUser) {
                    alertService.create(((MemberUser) user).getMember(), MemberAlert.Alerts.LOGIN_BLOCKED_BY_TRIES,
                            wrongAttempts, remoteAddress);
                }
                // Notify the user
                memberNotificationHandler.blockedCredentialsNotification(user, Credentials.LOGIN_PASSWORD);

                // Set the block time and clear previous attempts
                user.setPasswordBlockedUntil(blockTime.add(Calendar.getInstance()));
                wrongCredentialAttemptsDao.clear(user, Credentials.LOGIN_PASSWORD);

                throw new BlockedCredentialsException(Credentials.LOGIN_PASSWORD, user);
            }
            throw new InvalidCredentialsException(Credentials.LOGIN_PASSWORD, user);
        }

        // Everything ok - remove any previous wrong attempts
        wrongCredentialAttemptsDao.clear(user, Credentials.LOGIN_PASSWORD);
        return user;
    }

    private MemberUser checkPin(MemberUser user, final String plainPin, final String channel,
            final Member relatedMember) {
        user = fetchService.fetch(user,
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));

        // Check if not already blocked
        if (isBlocked(user.getPinBlockedUntil())) {
            throw new BlockedCredentialsException(Credentials.PIN, user);
        }

        final Member member = user.getMember();
        final String userPin = user.getPin();
        final String pin = hashHandler.hash(user.getSalt(), plainPin);

        // Check the pin
        if (userPin == null || !userPin.equalsIgnoreCase(StringUtils.trimToNull(pin))) {
            final MemberGroupSettings memberSettings = member.getMemberGroup().getMemberSettings();
            final int maxTries = memberSettings.getMaxPinWrongTries();
            wrongCredentialAttemptsDao.record(user, Credentials.PIN);
            int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), user, Credentials.PIN);
            if (wrongAttempts == maxTries) {
                final TimePeriod blockTime = memberSettings.getPinBlockTimeAfterMaxTries();
                final String relatedUsername = relatedMember == null ? "" : relatedMember.getUsername();

                // Create an alert and notify the member
                alertService.create(member, MemberAlert.Alerts.PIN_BLOCKED_BY_TRIES, maxTries, channel,
                        relatedUsername);
                memberNotificationHandler.blockedCredentialsNotification(user, Credentials.PIN);

                // Mark the pin as blocked, and clear previous wrong attempts
                user.setPinBlockedUntil(blockTime.add(Calendar.getInstance()));
                wrongCredentialAttemptsDao.clear(user, Credentials.PIN);

                throw new BlockedCredentialsException(Credentials.PIN, user);
            }
            throw new InvalidCredentialsException(Credentials.PIN, user);
        }

        // Everything ok - remove any previous wrong attempts
        wrongCredentialAttemptsDao.clear(user, Credentials.PIN);
        return user;
    }

    private User checkTransactionPassword(User user, final String plainTransactionPassword,
            final String remoteAddress) {
        user = fetchService.fetch(user,
                RelationshipHelper.nested(User.Relationships.ELEMENT, Element.Relationships.GROUP));

        // Check if not already blocked
        if (user.getTransactionPasswordStatus().equals(MemberUser.TransactionPasswordStatus.BLOCKED)) {
            throw new BlockedCredentialsException(Credentials.TRANSACTION_PASSWORD, user);
        }

        final String transactionPassword = hashHandler.hash(user.getSalt(),
                plainTransactionPassword == null ? null : plainTransactionPassword.toUpperCase());
        String userTransactionPassword = user.getTransactionPassword();
        if (userTransactionPassword == null || !userTransactionPassword.equalsIgnoreCase(transactionPassword)) {
            final Group group = user.getElement().getGroup();
            final BasicGroupSettings settings = group.getBasicSettings();
            final int maxTries = settings.getMaxTransactionPasswordWrongTries();
            wrongCredentialAttemptsDao.record(user, Credentials.TRANSACTION_PASSWORD);
            int wrongAttempts = wrongCredentialAttemptsDao.count(wrongAttemptsLimit(), user,
                    Credentials.TRANSACTION_PASSWORD);
            if (wrongAttempts == maxTries) {
                // Send an alert
                if (user instanceof AdminUser) {
                    alertService.create(SystemAlert.Alerts.ADMIN_TRANSACTION_PASSWORD_BLOCKED_BY_TRIES,
                            user.getUsername(), wrongAttempts, remoteAddress);
                } else {
                    Member m;
                    if (user instanceof MemberUser) {
                        m = ((MemberUser) user).getMember();
                    } else {
                        m = ((OperatorUser) user).getOperator().getMember();
                    }
                    alertService.create(m, MemberAlert.Alerts.TRANSACTION_PASSWORD_BLOCKED_BY_TRIES, wrongAttempts,
                            remoteAddress);
                    memberNotificationHandler.blockedCredentialsNotification(user,
                            Credentials.TRANSACTION_PASSWORD);
                }

                // Block the transaction password and clear previous attempts
                final ResetTransactionPasswordDTO dto = new ResetTransactionPasswordDTO();
                dto.setAllowGeneration(false);
                dto.setUser(user);
                resetTransactionPassword(dto);
                wrongCredentialAttemptsDao.clear(user, Credentials.TRANSACTION_PASSWORD);

                throw new BlockedCredentialsException(Credentials.TRANSACTION_PASSWORD, user);
            }
            throw new InvalidCredentialsException(Credentials.TRANSACTION_PASSWORD, user);
        }

        // Everything ok - remove any previous wrong attempts
        wrongCredentialAttemptsDao.clear(user, Credentials.TRANSACTION_PASSWORD);
        return user;
    }

    /**
     * Returns the session timeout for the given user
     */
    private TimePeriod getSessionTimeout(final User user, final boolean isPosWeb) {
        AccessSettings accessSettings = settingsService.getAccessSettings();
        TimePeriod sessionTimeout;
        if (isPosWeb) {
            sessionTimeout = accessSettings.getPoswebTimeout();
        } else if (user instanceof AdminUser) {
            sessionTimeout = accessSettings.getAdminTimeout();
        } else {
            sessionTimeout = accessSettings.getMemberTimeout();
        }
        return sessionTimeout;
    }

    private boolean isBlocked(final Calendar date) {
        return date != null && date.after(Calendar.getInstance());
    }

    private boolean isChannelEnabledForGroup(final Channel channel, MemberGroup memberGroup) {
        memberGroup = fetchService.fetch(memberGroup, MemberGroup.Relationships.CHANNELS);
        return memberGroup.getChannels().contains(channel);
    }

    private User loadUser(final String member, final String username) {
        if (StringUtils.isEmpty(member)) {
            // Normal user login
            final User user = elementService.loadUser(username, FETCH);
            if (user instanceof OperatorUser) {
                // Cannot login as operator without giving a member username too
                throw new EntityNotFoundException(User.class);
            }
            return user;
        } else {
            // Member's operator login - First load the member
            final User loadedMember = elementService.loadUser(member, FETCH);
            MemberUser memberUser;
            try {
                memberUser = (MemberUser) loadedMember;
            } catch (final ClassCastException e) {
                throw new EntityNotFoundException(MemberUser.class);
            }
            final Member m = memberUser.getMember();
            // Then the operator
            final OperatorUser user = elementService.loadOperatorUser(m, username, FETCH);
            // Assign the already fetched member to the operator
            user.getOperator().setMember(m);
            return user;
        }
    }

    private boolean requestTransactionPassword(User user) {
        user = fetchService.fetch(user, FETCH);
        Group group = user.getElement().getGroup();
        if (group instanceof OperatorGroup) {
            group = fetchService.fetch(group,
                    RelationshipHelper.nested(OperatorGroup.Relationships.MEMBER, Element.Relationships.GROUP));
        }
        final BasicGroupSettings settings = group.getBasicSettings();
        return settings.getTransactionPassword().isUsed();
    }

    private ValidationError resolveValidationError(final boolean loginPassword, final boolean numeric,
            final Element element, final Object object, final Object property, final String credential) {
        if (StringUtils.isEmpty(credential)) {
            return null;
        }

        final Group group = fetchService.fetch(element.getGroup());
        final BasicGroupSettings settings = group.getBasicSettings();
        RangeConstraint length;
        if (loginPassword) {
            length = settings.getPasswordLength();
        } else {
            final MemberGroup memberGroup = (MemberGroup) group;
            length = memberGroup.getMemberSettings().getPinLength();
        }

        // Validate the password length
        final ValidationError lengthResult = new LengthValidation(length).validate(object, property, credential);
        if (lengthResult != null) {
            return lengthResult;
        }

        final String keyPrefix = loginPassword ? "changePassword.error." : "changePin.error.";

        // Check for characters
        if (numeric && !(group instanceof AdminGroup)) {
            // Must be numeric
            if (!StringUtils.isNumeric(credential)) {
                return new ValidationError(keyPrefix + "mustBeNumeric");
            }
        }

        if (loginPassword && !numeric) {
            // When the virtual keyboard is enabled, make sure that the login password has no special characters
            final AccessSettings accessSettings = settingsService.getAccessSettings();
            if (accessSettings.isVirtualKeyboard() && StringHelper.hasSpecial(credential)) {
                return new ValidationError("changePassword.error.mustContainOnlyLettersOrNumbers");
            }
        }

        // Validate the password policy
        final PasswordPolicy policy = settings.getPasswordPolicy();
        if (policy == null || policy == PasswordPolicy.NONE) {
            // Nothing to enforce
            return null;
        }

        // Check for characters
        if (loginPassword && !numeric) {
            // Keys are hard coded with changePassword.error because pin is always numeric
            switch (policy) {
            case AVOID_OBVIOUS_LETTERS_NUMBERS:
                // Must include letters and numbers
                if (!StringHelper.hasDigits(credential) || !StringHelper.hasLetters(credential)) {
                    return new ValidationError("changePassword.error.mustIncludeLettersNumbers");
                }
                break;
            case AVOID_OBVIOUS_LETTERS_NUMBERS_SPECIAL:
                // Must include letters, numbers and special characters
                if (!StringHelper.hasDigits(credential) || !StringHelper.hasLetters(credential)
                        || !StringHelper.hasSpecial(credential)) {
                    return new ValidationError("changePassword.error.mustIncludeLettersNumbersSpecial");
                }
                break;
            }
        }

        // Check for obvious password
        if (isObviousCredential(element, credential)) {
            return new ValidationError(keyPrefix + "obvious");
        }

        return null;
    }

    private void updateSessionExpiration(final Long id, final Calendar newExpiration) {
        transactionHelper.runAsync(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(final TransactionStatus status) {
                sessionDao.updateExpiration(id, newExpiration);
            }
        });
    }

}