class MembershipProvider:
    """
    Provides business logic to provide user authentication.
    Generic MembershipProvider utilized to manage Membership for an application.
    It can be used to handle global authentication; or per-area authentication.
    Contains business logic for Login, Logout, ChangePassword.
    """

    defaults = {
        "store": None,
        "short_time_expiration": 1e3 * 60 * 20,
        "long_time_expiration": 1e3 * 60 * 60 * 24 * 365,
        "failed_login_attempts_limit": 4,
        "minutes_limit": 15,
        "requires_account_confirmation": False
    }

    def __init__(self, options=None):
        if options is None:
            options = {}
        params = dict(self.defaults, **options)
        self.validate_store_option(params)
        self.options = Bunch()
        self.options.merge(params)

    @staticmethod
    def get_hash(password, salt):
        """
        Returns an hashed version of password, created using the given salt
        :param salt: salt to use to hash a password
        :return: hashed version of password
        """
        key = (password + salt).encode("utf-8")
        return hashlib.sha224(key).hexdigest()

    @staticmethod
    def get_new_salt():
        """
        Returns a new salt to be used to hash password
        :return: {String} new salt to be used to hash passwords
        """
        ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        return ''.join(random.choice(ALPHABET) for i in range(16))

    @staticmethod
    def validate_store_option(params):
        """
        Validates the store option passed when instantiating a MembershipProvider.
        :param params: constructor options
        """
        if params["store"] is None:
            raise Exception("Missing `store` option")
        req = [
            "get_account", "get_accounts", "get_session", "create_account",
            "update_account", "create_session", "destroy_session",
            "save_session_data", "get_session_data",
            "get_failed_login_attempts", "save_login_attempt"
        ]
        for name in req:
            if not hasattr(params["store"], name):
                raise Exception("The given store does not implement `" + name +
                                "` member")

    def get_account(self, userkey):
        """
        Gets the account with the given key.
        :param userkey: key of the user (e.g. email or username)
        """
        data = self.options.store.get_account(userkey)
        if data is None:
            return None
        result = Bunch()
        result.merge(data)
        return result

    def get_account_by_id(self, account_id):
        """
        Gets the account details by id
        :param id: account id
        :return: account
        """
        data = self.options.store.get_account_by_id(account_id)
        if data is None:
            return None
        del data["salt"]
        del data["hash"]
        result = Bunch()
        result.merge(data)
        return result

    def get_accounts(self, options):
        """
        Gets the list of all application accounts.
        """
        # define searchable properties
        options["search_properties"] = ["email", "roles"]
        data = self.options.store.get_accounts(options)
        # NB !!!
        # Salt and hashed password must be kept private in this case.
        for item in data.subset:
            self.prepare_account_data(item)
        return data

    def get_sessions(self, options):
        """
        Gets the list of current user sessions.
        """
        # define searchable properties
        options["search_properties"] = [
            "email", "client_data.user_agent", "client_ip"
        ]
        data = self.options.store.get_sessions(options)
        for o in data.subset:
            if o["client_data"]:
                o["user_agent"] = o["client_data"]["user_agent"]
            else:
                o["user_agent"] = ""
            del o["client_data"]
        return data

    def prepare_account_data(self, account):
        """
        Prepares account data, to share it outside of bll.
        Salt and hashed password must be never get out of bll.
        """
        del account["salt"]
        del account["hash"]
        if "roles" not in account:
            account["roles"] = []
        return account

    def create_account(self, userkey, password, data=None, roles=None):
        """
        Creates a new user account in db
        :param userkey: key of the user (e.g. email or username)
        :param password: account clear password (e.g. user defined password)
        :param data: dict, optional account data
        :return:
        """
        # verify that an account with the same key doesn't exist already
        account_data = self.options.store.get_account(userkey)
        if account_data is not None:
            return False, "AccountAlreadyExisting"
        if data is None:
            data = {}
        if roles is None:
            roles = []
        salt = self.get_new_salt()
        hashedpassword = self.get_hash(password, salt)
        now = datetime.datetime.now()

        if self.options.requires_account_confirmation:
            # set a confirmation token inside the account data
            data["confirmation_token"] = uuid.uuid1()
        return True, self.options.store.create_account(userkey, hashedpassword,
                                                       salt, data, roles)

    def update_password(self, userkey, password):
        """
        Updates the password for the account with the given key.
        :param userkey: key of the user (e.g. email or username)
        :param password: account clear password (e.g. user defined password)
        :return:
        """
        account_data = self.options.store.get_account(userkey)
        if account_data is None:
            return False, "Account not found"
        salt = account_data["salt"]
        hashedpassword = self.get_hash(password, salt)
        now = datetime.datetime.now()
        self.options.store.update_account(userkey, {"hash": hashedpassword})
        return True, ""

    def delete_account(self, userkey):
        """
        Deletes the account with the given userkey
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        account = self.options.store.get_account(userkey)
        if account is None:
            return False, "AccountNotFound"
        self.options.store.delete_account(userkey)
        return True

    def confirm_account(self, userkey):
        """
        Confirms the account with the given userkey.
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        return self.update_account(userkey, {"confirmed": True})

    def ban_account(self, userkey):
        """
        Bans the account with the given userkey.
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        return self.update_account(userkey, {"banned": True})

    def update_account(self, userkey, data):
        """
        Updates the account with the given key; setting the data.
        :param userkey: the user key (email address or username)
        :param data: account data to update
        :return: self
        """
        account = self.options.store.get_account(userkey)
        if account is None:
            return False, "AccountNotFound"
        self.options.store.update_account(userkey, data)
        return True, None

    def try_login(self,
                  userkey,
                  password,
                  remember,
                  client_ip,
                  client_data=None,
                  options=None):
        """
        Tries to perform a login for the user with the given key (e.g. email or username); password;
        :param userkey: the user key (email address or username)
        :param password:
        :param remember: whether to have a longer expiration time or not
        :param client_ip: ip of the client for which the function has been called
        :param client_data: optional client data
        :param options: extra options
        :return:
        """
        # get account data
        account_data = self.get_account(userkey)
        if account_data is None:
            return False, "AccountNotFound"

        login_attempts = self.get_failed_login_attempts(userkey)
        tooManyAttempts = self.options.failed_login_attempts_limit <= login_attempts
        if tooManyAttempts:
            return False, "TooManyAttempts"
            # error: too many attempts in the last minutes, for this user

        check_password = options is None or options[
            "automatic_no_password"] is None

        if check_password:
            # generate hash of given password, appending salt
            hsh = self.get_hash(password, account_data.salt)
            if account_data.hash != hsh:
                # the key exists, but the password is wrong
                self.report_login_attempt(userkey, client_ip)
                # exit
                return False, "WrongPassword"

        # check if the account is confirmed
        if self.options.requires_account_confirmation and not account_data.confirmed:
            return False, "RequireConfirmation"

        # check if the account was banned
        if hasattr(account_data, "banned") and account_data.banned == True:
            return False, "BannedAccount"

        # get session expiration
        expiration = self.get_new_expiration(remember)
        # save session
        session = self.options.store.create_session(userkey, expiration,
                                                    client_ip, client_data)

        del account_data.salt
        del account_data.hash
        return True, {
            "principal": Principal(account_data.id, account_data, session,
                                   True),
            "session": Session.from_dict(session)
        }

    def report_login_attempt(self, userkey, client_ip):
        """
        Reports a login attempt.
        :param userkey: the user key (email address or username)
        :param client_ip: ip of the client
        """
        now = datetime.datetime.now()
        self.options.store.save_login_attempt(userkey, client_ip, now)
        return self

    def change_password(self, userkey, password_reset_key, new_password):
        pass

    def get_failed_login_attempts(self, userkey):
        """
        Gets the number of failed login attempts for a userkey in the amount of minutes defined by MinutesLimit option.
        :param userkey: the user key (email address or username)
        """
        now = datetime.datetime.now()
        ms = self.options.minutes_limit * 60 * 1e3
        start = now - datetime.timedelta(milliseconds=ms)
        count = self.options.store.get_failed_login_attempts(
            userkey, start, now)
        return count

    def try_login_by_session_key(self, sessionkey):
        """
        Tries to perform login by user session key.
        :param sessionkey:
        :return: boolean, session, account
        """
        # returns bool, principal
        if sessionkey is None:
            return False, None

        session = self.options.store.get_session(sessionkey)
        if session is None:
            return False, None

        # convert into a class
        session = Session.from_dict(session)

        now = datetime.datetime.now()
        if session.expiration < now:
            return False, None

        if session.anonymous:
            return True, {
                "principal": Principal(None, None, session, False),
                "session": session
            }

        # get account data
        account = self.options.store.get_account(session.userkey)

        if account is None:
            return False, None

        # return session and account data
        return True, {
            "principal": Principal(account["id"], account, session, True),
            "session": session
        }

    def initialize_anonymous_session(self, client_ip, client_data):
        """
        Initializes a session for an anonymous user.
        :param client_ip:
        :param navigator:
        :return:
        """
        expiration = self.get_new_expiration(True)
        userkey = None
        # save session
        session = self.options.store.create_session(userkey, expiration,
                                                    client_ip, client_data)
        return {
            "principal": Principal(None, None, session, False),
            "session": Session.from_dict(session)
        }

    def get_new_expiration(self, remember=None):
        """
        Returns the expiration for a new session, based on the provider settings and if the user wants to be remembered.
        for longer or not.
        :param remember: boolean
        :return: datetime
        """
        if remember is None:
            remember = False
        now = datetime.datetime.now()
        ms = self.options.long_time_expiration if remember else self.options.short_time_expiration
        expiration = now + datetime.timedelta(milliseconds=ms)
        return expiration

    def save_session_data(self, sessionkey, data):
        """
        Stores session data in the database.
        :param sessionkey: the key of the session
        :param data: dictionary of data to store in database
        :return:
        """
        self.options.store.save_session_data(sessionkey, data)
        return self

    def get_session_data(self, sessionkey):
        """
        Gets the data associated with the given session, in database.
        :param sessionkey: the key of the session
        :return: dict
        """
        return self.options.get_session_data(sessionkey)

    def destroy_session(self, sessionkey):
        """
        Destroys the session with the given key.
        :param sessionkey: key of the session to destroy.
        :return: self
        """
        self.options.store.destroy_session(sessionkey)
        return self

    @staticmethod
    def validate_password(password_one, password_two):
        """
        Validates the two given passwords.
        :param password_one: first password written by user.
        :param password_two: password confirmation.
        :return: success, error
        """
        if not password_one or not password_two:
            return False, "missing password"
        if password_one != password_two:
            return False, "password mismatch"
        v = password_two
        rx = {
            "enforce":
            "^(?=.*\d)(?=.*[a-zA-Z\.\,\;\@\$\!\?\#\*\-\_\%\&]).{9,}$"
        }
        if not re.match(rx["enforce"], v):
            return False, "password too weak"
        return True, None
class MembershipProvider:
    """
    Provides business logic to provide user authentication.
    Generic MembershipProvider utilized to manage Membership for an application.
    It can be used to handle global authentication; or per-area authentication.
    Contains business logic for Login, Logout, ChangePassword.
    """

    defaults =  {
      "store": None,
      "short_time_expiration": 1e3 * 60 * 20,
      "long_time_expiration": 1e3 * 60 * 60 * 24 * 365,
      "failed_login_attempts_limit": 4,
      "minutes_limit": 15,
      "requires_account_confirmation": False
    }


    def __init__(self, options = None):
        if options is None:
            options = {}
        params = dict(self.defaults, **options)
        self.validate_store_option(params)
        self.options = Bunch()
        self.options.merge(params)


    @staticmethod
    def get_hash(password, salt):
        """
        Returns an hashed version of password, created using the given salt
        :param salt: salt to use to hash a password
        :return: hashed version of password
        """
        key = (password + salt).encode("utf-8")
        return hashlib.sha224(key).hexdigest()


    @staticmethod
    def get_new_salt():
        """
        Returns a new salt to be used to hash password
        :return: {String} new salt to be used to hash passwords
        """
        ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        return ''.join(random.choice(ALPHABET) for i in range(16))


    @staticmethod
    def validate_store_option(params):
        """
        Validates the store option passed when instantiating a MembershipProvider.
        :param params: constructor options
        """
        if params["store"] is None:
          raise Exception("Missing `store` option")
        req = ["get_account",
               "get_accounts",
               "get_session",
               "create_account",
               "update_account",
               "create_session",
               "destroy_session",
               "save_session_data",
               "get_session_data",
               "get_failed_login_attempts",
               "save_login_attempt"]
        for name in req:
            if not hasattr(params["store"], name):
                raise Exception("The given store does not implement `" + name + "` member")


    def get_account(self, userkey):
        """
        Gets the account with the given key.
        :param userkey: key of the user (e.g. email or username)
        """
        data = self.options.store.get_account(userkey)
        if data is None:
            return None
        result = Bunch()
        result.merge(data)
        return result


    def get_account_by_id(self, account_id):
        """
        Gets the account details by id
        :param id: account id
        :return: account
        """
        data = self.options.store.get_account_by_id(account_id)
        if data is None:
            return None
        del data["salt"]
        del data["hash"]
        result = Bunch()
        result.merge(data)
        return result


    def get_accounts(self, options):
        """
        Gets the list of all application accounts.
        """
        # define searchable properties
        options["search_properties"] = ["email", "roles"]
        data = self.options.store.get_accounts(options)
        # NB !!!
        # Salt and hashed password must be kept private in this case.
        for item in data.subset:
            self.prepare_account_data(item)
        return data


    def get_sessions(self, options):
        """
        Gets the list of current user sessions.
        """
        # define searchable properties
        options["search_properties"] = ["email", "client_data.user_agent", "client_ip"]
        data = self.options.store.get_sessions(options)
        for o in data.subset:
            if o["client_data"]:
                o["user_agent"] = o["client_data"]["user_agent"]
            else:
                o["user_agent"] = ""
            del o["client_data"]
        return data


    def prepare_account_data(self, account):
        """
        Prepares account data, to share it outside of bll.
        Salt and hashed password must be never get out of bll.
        """
        del account["salt"]
        del account["hash"]
        if "roles" not in account:
            account["roles"] = []
        return account


    def create_account(self, userkey, password, data = None, roles = None):
        """
        Creates a new user account in db
        :param userkey: key of the user (e.g. email or username)
        :param password: account clear password (e.g. user defined password)
        :param data: dict, optional account data
        :return:
        """
        # verify that an account with the same key doesn't exist already
        account_data = self.options.store.get_account(userkey)
        if account_data is not None:
            return False, "AccountAlreadyExisting"
        if data is None:
            data = {}
        if roles is None:
            roles = []
        salt = self.get_new_salt()
        hashedpassword = self.get_hash(password, salt)
        now = datetime.datetime.now()

        if self.options.requires_account_confirmation:
            # set a confirmation token inside the account data
            data["confirmation_token"] = uuid.uuid1()
        return True, self.options.store.create_account(userkey, hashedpassword, salt, data, roles)


    def update_password(self, userkey, password):
        """
        Updates the password for the account with the given key.
        :param userkey: key of the user (e.g. email or username)
        :param password: account clear password (e.g. user defined password)
        :return:
        """
        account_data = self.options.store.get_account(userkey)
        if account_data is None:
            return False, "Account not found"
        salt = account_data["salt"]
        hashedpassword = self.get_hash(password, salt)
        now = datetime.datetime.now()
        self.options.store.update_account(userkey, { "hash": hashedpassword })
        return True, ""


    def delete_account(self, userkey):
        """
        Deletes the account with the given userkey
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        account = self.options.store.get_account(userkey)
        if account is None:
            return False, "AccountNotFound"
        self.options.store.delete_account(userkey)
        return True


    def confirm_account(self, userkey):
        """
        Confirms the account with the given userkey.
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        return self.update_account(userkey, {
            "confirmed": True
        })


    def ban_account(self, userkey):
        """
        Bans the account with the given userkey.
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        return self.update_account(userkey, {
            "banned": True
        })


    def update_account(self, userkey, data):
        """
        Updates the account with the given key; setting the data.
        :param userkey: the user key (email address or username)
        :param data: account data to update
        :return: self
        """
        account = self.options.store.get_account(userkey)
        if account is None:
            return False, "AccountNotFound"
        self.options.store.update_account(userkey, data)
        return True, None


    def try_login(self, userkey, password, remember, client_ip, client_data = None, options = None):
        """
        Tries to perform a login for the user with the given key (e.g. email or username); password;
        :param userkey: the user key (email address or username)
        :param password:
        :param remember: whether to have a longer expiration time or not
        :param client_ip: ip of the client for which the function has been called
        :param client_data: optional client data
        :param options: extra options
        :return:
        """
        # get account data
        account_data = self.get_account(userkey)
        if account_data is None:
            return False, "AccountNotFound"

        login_attempts = self.get_failed_login_attempts(userkey)
        tooManyAttempts = self.options.failed_login_attempts_limit <= login_attempts
        if tooManyAttempts:
            return False, "TooManyAttempts"
            # error: too many attempts in the last minutes, for this user

        check_password = options is None or options["automatic_no_password"] is None

        if check_password:
            # generate hash of given password, appending salt
            hsh = self.get_hash(password, account_data.salt)
            if account_data.hash != hsh:
                # the key exists, but the password is wrong
                self.report_login_attempt(userkey, client_ip)
                # exit
                return False, "WrongPassword"

        # check if the account is confirmed
        if self.options.requires_account_confirmation and not account_data.confirmed:
            return False, "RequireConfirmation"

        # check if the account was banned
        if hasattr(account_data, "banned") and account_data.banned == True:
            return False, "BannedAccount"

        # get session expiration
        expiration = self.get_new_expiration(remember)
        # save session
        session = self.options.store.create_session(userkey, expiration, client_ip, client_data)

        del account_data.salt
        del account_data.hash
        return True, {
            "principal": Principal(account_data.id,
                                   account_data,
                                   session,
                                   True),
            "session": Session.from_dict(session)
        }

    def report_login_attempt(self, userkey, client_ip):
        """
        Reports a login attempt.
        :param userkey: the user key (email address or username)
        :param client_ip: ip of the client
        """
        now = datetime.datetime.now()
        self.options.store.save_login_attempt(userkey, client_ip, now)
        return self


    def change_password(self, userkey, password_reset_key, new_password):
        pass


    def get_failed_login_attempts(self, userkey):
        """
        Gets the number of failed login attempts for a userkey in the amount of minutes defined by MinutesLimit option.
        :param userkey: the user key (email address or username)
        """
        now = datetime.datetime.now()
        ms = self.options.minutes_limit * 60 * 1e3
        start = now - datetime.timedelta(milliseconds=ms)
        count = self.options.store.get_failed_login_attempts(userkey, start, now)
        return count


    def try_login_by_session_key(self, sessionkey):
        """
        Tries to perform login by user session key.
        :param sessionkey:
        :return: boolean, session, account
        """
        # returns bool, principal
        if sessionkey is None:
            return False, None

        session = self.options.store.get_session(sessionkey)
        if session is None:
            return False, None

        # convert into a class
        session = Session.from_dict(session)

        now = datetime.datetime.now()
        if session.expiration < now:
            return False, None

        if session.anonymous:
            return True, {
            "principal": Principal(None,
                                   None,
                                   session,
                                   False),
            "session": session
        }

        # get account data
        account = self.options.store.get_account(session.userkey)

        if account is None:
            return False, None

        # return session and account data
        return True, {
            "principal": Principal(account["id"],
                                   account,
                                   session,
                                   True),
            "session": session
        }


    def initialize_anonymous_session(self, client_ip, client_data):
        """
        Initializes a session for an anonymous user.
        :param client_ip:
        :param navigator:
        :return:
        """
        expiration = self.get_new_expiration(True)
        userkey = None
        # save session
        session = self.options.store.create_session(userkey, expiration, client_ip, client_data)
        return {
            "principal": Principal(None, None, session, False),
            "session": Session.from_dict(session)
        }


    def get_new_expiration(self, remember = None):
        """
        Returns the expiration for a new session, based on the provider settings and if the user wants to be remembered.
        for longer or not.
        :param remember: boolean
        :return: datetime
        """
        if remember is None:
            remember = False
        now = datetime.datetime.now()
        ms = self.options.long_time_expiration if remember else self.options.short_time_expiration
        expiration = now + datetime.timedelta(milliseconds=ms)
        return expiration


    def save_session_data(self, sessionkey, data):
        """
        Stores session data in the database.
        :param sessionkey: the key of the session
        :param data: dictionary of data to store in database
        :return:
        """
        self.options.store.save_session_data(sessionkey, data)
        return self


    def get_session_data(self, sessionkey):
        """
        Gets the data associated with the given session, in database.
        :param sessionkey: the key of the session
        :return: dict
        """
        return self.options.get_session_data(sessionkey)


    def destroy_session(self, sessionkey):
        """
        Destroys the session with the given key.
        :param sessionkey: key of the session to destroy.
        :return: self
        """
        self.options.store.destroy_session(sessionkey)
        return self


    @staticmethod
    def validate_password(password_one, password_two):
        """
        Validates the two given passwords.
        :param password_one: first password written by user.
        :param password_two: password confirmation.
        :return: success, error
        """
        if not password_one or not password_two:
            return False, "missing password"
        if password_one != password_two:
            return False, "password mismatch"
        v = password_two
        rx = {
          "enforce": "^(?=.*\d)(?=.*[a-zA-Z\.\,\;\@\$\!\?\#\*\-\_\%\&]).{9,}$"
        }
        if not re.match(rx["enforce"], v):
            return False, "password too weak"
        return True, None
class MembershipProvider:
    """
    Provides business logic to provide user authentication.

    It can be used to handle global authentication; or per-area authentication.
    Contains business logic for Login, Logout, ChangePassword.
    """

    defaults = {
        "host": None,  # the host used when generating emails
        "short_time_expiration": 1e3 * 60 * 20,
        "long_time_expiration": 1e3 * 60 * 60 * 24 * 365,
        "failed_login_attempts_limit": 4,
        "minutes_limit": 15,
        "requires_account_confirmation": False
    }

    def __init__(self):
        store = self.get_membership_store()
        options = self.get_options()
        require_params(store=store)
        if options is None:
            options = {}
        params = dict(self.defaults, **options)
        self.validate_store(store)
        self.store = store
        self.options = Bunch()
        self.options.merge(params)
        self.principal_type = self.get_principal_type()

    def get_membership_store(self):
        """
        Returns the membership store used by this membership provider.

        This method must be implemented in subclasses.
        """
        name = type(self).__name__
        raise NotImplementedError(
            "{} does not implement 'get_membership_store'.".format(name))

    def get_options(self):
        """
        Returns options to override the default parameters.
        """
        return {}

    def get_principal_type(self):
        """
        Returns the membership store used by this membership provider.

        This method can be implemented to set specific Principal types by implementation.
        """
        return Principal

    @staticmethod
    def get_hash(password, salt):
        """
        Returns an hashed version of password, created using the given salt

        :param password: original password, to obfuscate with the given salt
        :param salt: salt to use to hash a password
        :return: hashed version of password
        """
        key = (password + salt).encode("utf-8")
        return hashlib.sha224(key).hexdigest()

    @staticmethod
    def get_new_salt():
        """
        Returns a new salt to be used to hash password

        :return: {String} new salt to be used to hash passwords
        """
        alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        return ''.join(random.choice(alphabet) for _ in range(16))

    @staticmethod
    def validate_store(store):
        """
        Validates the store option passed when instantiating a MembershipProvider.
        :param store: store
        """
        # TODO: use abstract class!?
        req = [
            "get_account", "get_accounts", "get_session",
            "get_session_by_guid", "create_account", "update_account",
            "create_session", "destroy_session", "save_session_data",
            "get_session_data", "get_failed_login_attempts",
            "save_login_attempt"
        ]
        for name in req:
            if not hasattr(store, name):
                raise Exception("The given store does not implement `" + name +
                                "` member")

    def get_account(self, userkey):
        """
        Gets the account with the given key.
        :param userkey: key of the user (e.g. email or username)
        """
        data = self.store.get_account(userkey)
        if data is None:
            return None
        result = Bunch()
        result.merge(data)
        return result

    def get_account_by_id(self, account_id):
        """
        Gets the account details by id
        :param account_id: account id
        :return: account
        """
        data = self.store.get_account_by_id(account_id)
        if data is None:
            return None
        del data["salt"]
        del data["hash"]
        result = Bunch()
        result.merge(data)
        return result

    def get_accounts(self, options):
        """
        Gets the list of all application accounts.
        """
        # define searchable properties
        options["search_properties"] = ["email", "roles"]
        data = self.store.get_accounts(options)
        # NB !!!
        # Salt and hashed password must be kept private in this case.
        for item in data.subset:
            self.prepare_account_data(item)
        return data

    def get_sessions(self, options):
        """
        Gets the list of current user sessions.
        """
        # define searchable properties
        options["search_properties"] = [
            "email", "client_data.user_agent", "client_ip"
        ]
        data = self.store.get_sessions(options)
        for o in data.subset:
            if o["client_data"]:
                o["user_agent"] = o["client_data"]["user_agent"]
            else:
                o["user_agent"] = ""
            del o["client_data"]
        return data

    def prepare_account_data(self, account):
        """
        Prepares account data, to share it outside of bll.
        Salt and hashed password must be never get out of bll.
        """
        del account["salt"]
        del account["hash"]
        if "roles" not in account:
            account["roles"] = []
        return account

    def validate_userkey(self, userkey):
        if userkey is None or re.match("^\s*$", userkey):
            return False
        return True

    def get_account_defaults(self):
        return {}

    def create_account(self,
                       userkey,
                       password,
                       data=None,
                       roles=None,
                       lang="en"):
        """
        Creates a new user account.

        :param userkey: key of the user (e.g. email or username)
        :param password: account clear password (e.g. user defined password)
        :param data: dict, optional account data
        :return:
        """
        if not self.validate_userkey(userkey) or not self.validate_password(
                password):
            return False, "InvalidParameter"

        # verify that an account with the same key doesn't exist already
        account_data = self.store.get_account(userkey)
        if account_data is not None:
            return False, "AccountAlreadyExisting"
        if data is None:
            data = {}
        if roles is None:
            roles = []
        salt = self.get_new_salt()
        hashedpassword = self.get_hash(password, salt)
        data = dict(self.get_account_defaults(), **data)

        if self.options.requires_account_confirmation:
            # set a confirmation token inside the account data
            confirmation_token = str(uuid.uuid1())
            data["confirmation_token"] = confirmation_token

        account_data = self.store.create_account(userkey, hashedpassword, salt,
                                                 data, roles)

        # TODO: if desired, implement send email logic
        return True, account_data

    def is_password_correct(self, account_id, password):
        require_params(account_id=account_id, password=password)

        if not self.validate_password(password):
            return False

        account_data = self.store.get_account_by_id(account_id)
        if account_data is None:
            raise ValueError("AccountNotFound")

        hsh = self.get_hash(password, account_data["salt"])
        if account_data["hash"] != hsh:
            return False
        return True

    def update_password(self, userkey, password):
        """
        Updates the password for the account with the given key.

        :param userkey: key of the user (e.g. email or username)
        :param password: account clear password (e.g. user defined password)
        :return:
        """
        if not self.validate_userkey(userkey) or not self.validate_password(
                password):
            return False, "InvalidParameter"

        account_data = self.store.get_account(userkey)
        if account_data is None:
            return False, "Account not found"
        salt = account_data["salt"]
        hashedpassword = self.get_hash(password, salt)
        self.store.update_account(userkey, {"hash": hashedpassword})
        return True, ""

    def delete_account(self, userkey):
        """
        Deletes the account with the given userkey
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        account = self.store.get_account(userkey)
        if account is None:
            return False, "AccountNotFound"
        self.store.delete_account(userkey)
        return True, None

    def delete_account_with_validation(self,
                                       account_id,
                                       current_password,
                                       lang="en"):
        """
        Deletes the account with the given id, validating its password.
        :param account_id:
        :param current_password:
        :return:
        """
        account = self.store.get_account_by_id(account_id)
        if account is None:
            return False, "AccountNotFound"
        hsh = self.get_hash(current_password, account["salt"])
        if account.get("hash") != hsh:
            return False, "InvalidPassword"

        # delete the account
        deleted = self.store.delete_account_by_id(account_id)
        if not deleted:
            return False, "NoDocumentDeleted"

        # TODO: if desired, implement farewell email here
        return True, None

    def confirm_account(self, account_id, confirmation_token):
        """
        Confirms the account with the given id and using the given confirmation token.
        """
        if not account_id or account_id.isspace():
            raise ArgumentNullException("account_id")
        if not confirmation_token or confirmation_token.isspace():
            raise ArgumentNullException("confirmation_token")

        account_data = self.store.get_account_by_id(account_id)
        if not account_data:
            raise ValueError("Account not found")

        if account_data.get("confirmed") is True:
            return True

        if account_data.get("confirmation_token") != confirmation_token:
            raise ValueError("Invalid confirmation token for account `%s`" %
                             account_id)

        self.store.update_account_by_id(account_id, {"confirmed": True},
                                        unset_data={"confirmation_token": ""})
        return True

    def ban_account(self, userkey):
        """
        Bans the account with the given userkey.
        :param userkey: the user key (email address or username)
        :return: success, error
        """
        return self.update_account(userkey, {"banned": True})

    def update_account(self, userkey, data):
        """
        Updates the account with the given key; setting the data.
        :param userkey: the user key (email address or username)
        :param data: account data to update
        :return: self
        """
        account = self.store.get_account(userkey)
        if account is None:
            return False, "AccountNotFound"
        self.store.update_account(userkey, data)
        return True, None

    def try_login(self,
                  userkey,
                  password,
                  remember,
                  client_ip,
                  client_data=None,
                  check_password=True):
        """
        Tries to perform a login for the user with the given key (e.g. email or username); password;
        :param userkey: the user key (email address or username)
        :param password:
        :param remember: whether to have a longer expiration time or not
        :param client_ip: ip of the client for which the function has been called
        :param client_data: optional client data
        :param options: extra options
        :return:
        """
        # get account data
        account_data = self.get_account(userkey)
        if account_data is None:
            return False, "WrongCombo"

        login_attempts = self.get_failed_login_attempts(userkey)
        too_many_attempts = self.options.failed_login_attempts_limit <= login_attempts
        if too_many_attempts:
            # log information

            return False, "TooManyAttempts"
            # error: too many attempts in the last minutes, for this user

        if check_password:
            # generate hash of given password, appending salt
            hsh = self.get_hash(password, account_data.salt)
            if account_data.hash != hsh:
                # the key exists, but the password is wrong
                self.report_login_attempt(userkey, client_ip)
                # exit
                return False, "WrongCombo"

        # check if the account is confirmed
        if self.options.requires_account_confirmation and not account_data.confirmed:
            return False, "RequireConfirmation"

        # check if the account was banned
        if hasattr(account_data, "banned") and account_data.banned is True:
            return False, "BannedAccount"

        # get session expiration
        expiration = self.get_new_expiration(remember)
        # save session
        session = self.store.create_session(userkey, expiration, client_ip,
                                            client_data)
        session = Session.from_dict(session)

        del account_data.salt
        del account_data.hash
        principal = self.principal_type

        # TODO: if desired, implement sending of email here "new login from..."

        return True, AuthenticationResult(
            principal(account_data.id, account_data, session, True), session)

    def report_login_attempt(self, userkey, client_ip):
        """
        Reports a login attempt, storing it in the persistence layer.

        :param userkey: the user key (email address or username)
        :param client_ip: ip of the client
        """
        now = datetime.datetime.utcnow()
        self.store.save_login_attempt(userkey, client_ip, now)
        return self

    def validate_password_reset_token(self, account_id, token):
        account_data = self.store.get_account_by_id(account_id)
        if account_data is None:
            return False, "AccountNotFound"

        password_reset_token = account_data.get("passwordResetToken")
        if not password_reset_token or password_reset_token.isspace():
            return False, "MissingPasswordResetToken"

        if password_reset_token != token:
            return False, "InvalidToken"

        return True, None

    def change_password(self, account_id, current_password, password_one,
                        password_two):
        """
        This function is used when a user wants to change a password, using the current password
        """
        require_params(account_id=account_id,
                       current_password=current_password,
                       password_one=password_one,
                       password_two=password_two)

        account_data = self.store.get_account_by_id(account_id)
        if account_data is None:
            raise InvalidOperation("Account not found")

        # validate current password
        if not self.is_password_correct(account_id, current_password):
            raise ValueError("WrongPassword")

        # validate passwords
        valid_passwords, error = self.validate_passwords(
            password_one, password_two)
        if not valid_passwords:
            raise ValueError("Passwords are not valid: " + error)

        # update account password
        salt = self.get_new_salt()
        hashedpassword = self.get_hash(password_one, salt)
        # commit the change:
        self.store.change_password(account_id, hashedpassword, salt)
        return True, None

    def commit_password_reset(self, account_id, token, password_one,
                              password_two):
        """
        This function is used when a user requested a password change, using a token that was sent by email.
        It validates a token that was set previously.

        :param account_id: id of the account of which the password should be changed
        :param token: password reset token
        :param password_one: first password
        :param password_two: password repetition
        :return:
        """
        require_params(account_id=account_id,
                       token=token,
                       password_one=password_one,
                       password_two=password_two)

        account_data = self.store.get_account_by_id(account_id)
        if account_data is None:
            raise InvalidOperation("Account not found")

        password_reset_token = account_data.get("passwordResetToken")
        if not password_reset_token or password_reset_token.isspace():
            raise InvalidOperation("Password reset token not initialized")

        if password_reset_token != token:
            raise ValueError("Invalid password reset token")

        # validate passwords
        valid_passwords, error = self.validate_passwords(
            password_one, password_two)
        if not valid_passwords:
            raise ValueError("Passwords are not valid: " + error)

        salt = self.get_new_salt()
        hashedpassword = self.get_hash(password_one, salt)
        # commit the change:
        self.store.change_password(account_id, hashedpassword, salt)
        return True

    def get_failed_login_attempts(self, userkey):
        """
        Gets the number of failed login attempts for a userkey in the amount of minutes defined by MinutesLimit option.
        :param userkey: the user key (email address or username)
        """
        now = datetime.datetime.utcnow()
        ms = self.options.minutes_limit * 60 * 1e3
        start = now - datetime.timedelta(milliseconds=ms)
        count = self.store.get_failed_login_attempts(userkey, start, now)
        return count

    async def try_login_by_session_key(self, sessionkey):
        """
        Tries to perform login by user session key.
        :param sessionkey:
        :return: boolean, session, account
        """
        # returns bool, principal
        if sessionkey is None:
            return False, None

        session = await self.store.get_session_by_guid(sessionkey)
        if session is None:
            return False, None

        # convert into a class
        session = Session.from_dict(session)

        now = datetime.datetime.utcnow()
        if session.expiration < now:
            return False, None
        principal_type = self.principal_type
        if session.anonymous:
            return True, AuthenticationResult(
                principal_type(None, None, session, False), session)

        # get account data
        account = await self.store.get_account(session.userkey)

        if account is None:
            return False, None

        # return session and account data
        principal_type = self.principal_type
        return True, AuthenticationResult(
            principal_type(account["id"], account, session, True), session)

    async def initialize_anonymous_session(self, client_ip, client_data):
        """
        Initializes a session for an anonymous user.

        :param client_ip: ip of the client
        :param client_data: information about the client software
        :return:
        """
        expiration = self.get_new_expiration(True)
        user_id = None
        session_data = await self.store.create_session(user_id, expiration,
                                                       client_ip, client_data)
        session = Session.from_dict(session_data)
        principal_type = self.principal_type
        return AuthenticationResult(principal_type(None, None, session, False),
                                    session)

    def get_new_expiration(self, remember=None):
        """
        Returns the expiration for a new session, based on the provider settings and if the user wants to be remembered.
        for longer or not.

        :param remember: boolean
        :return: datetime
        """
        if remember is None:
            remember = False
        now = datetime.datetime.utcnow()
        ms = self.options.long_time_expiration if remember else self.options.short_time_expiration
        expiration = now + datetime.timedelta(milliseconds=ms)
        return expiration

    def save_session_data(self, sessionkey, data):
        """
        Stores session data in the database.
        :param sessionkey: the key of the session
        :param data: dictionary of data to store in database
        :return:
        """
        self.store.save_session_data(sessionkey, data)
        return self

    def get_session_data(self, sessionkey):
        """
        Gets the data associated with the given session, in database.
        :param sessionkey: the key of the session
        :return: dict
        """
        return self.options.get_session_data(sessionkey)

    def destroy_session(self, sessionkey):
        """
        Destroys the session with the given key.
        :param sessionkey: key of the session to destroy.
        :return: self
        """
        self.store.destroy_session(sessionkey)
        return self

    def validate_new_passwords(self, pass_one, pass_two):
        if pass_one != pass_two:
            return False
        if not self.validate_password(pass_one):
            return False
        return True

    def validate_passwords(self, password_one, password_two):
        """
        Validates the two given passwords.

        :param password_one: first password written by user.
        :param password_two: password confirmation.
        :return: success, error
        """
        if not password_one or not password_two:
            return False, "missing password"
        if password_one != password_two:
            return False, "password mismatch"
        v = password_two
        valid = self.validate_password(v)
        if not valid:
            return False, "password too weak"
        return True, None

    def validate_password(self, password):
        """
        Validates a single value to see if it's suitable for a password.

        :param password: value to validate.
        """
        if not password or password.isspace():
            return False
        l = len(password)
        if l < 6:
            # too simple, either way
            return False

        if l > 50:
            # too long: the interface allows maximum 50 chars
            return False

        keys = OrderedDict.fromkeys(password).keys()
        keys_length = len(keys)

        if keys_length < 3:
            # the password is too weak, it contains less than 3 different types of character
            return False

        forbidden = {"password", "qwerty", "123456", "1234567"}
        if password.lower() in forbidden:
            return False
        return True