Exemplo n.º 1
0
    def test_25_verify_and_update(self):
        "test verify_and_update()"
        cc = CryptContext(**self.sample_policy_1)

        #create some hashes
        h1 = cc.encrypt("password", scheme="des_crypt")
        h2 = cc.encrypt("password", scheme="sha256_crypt")

        #check bad password, deprecated hash
        ok, new_hash = cc.verify_and_update("wrongpass", h1)
        self.assertFalse(ok)
        self.assertIs(new_hash, None)

        #check bad password, good hash
        ok, new_hash = cc.verify_and_update("wrongpass", h2)
        self.assertFalse(ok)
        self.assertIs(new_hash, None)

        #check right password, deprecated hash
        ok, new_hash = cc.verify_and_update("password", h1)
        self.assertTrue(ok)
        self.assertTrue(cc.identify(new_hash), "sha256_crypt")

        #check right password, good hash
        ok, new_hash = cc.verify_and_update("password", h2)
        self.assertTrue(ok)
        self.assertIs(new_hash, None)
Exemplo n.º 2
0
    def test_90_bcrypt_normhash(self):
        "teset verify_and_update / hash_needs_update corrects bcrypt padding"
        # see issue 25.
        bcrypt = hash.bcrypt
        
        PASS1 = "loppux"
        BAD1  = "$2a$12$oaQbBqq8JnSM1NHRPQGXORm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"
        GOOD1 = "$2a$12$oaQbBqq8JnSM1NHRPQGXOOm4GCUMqp7meTnkft4zgSnrbhoKdDV0C"
        ctx = CryptContext(["bcrypt"])
        
        with catch_warnings(record=True) as wlog:
            warnings.simplefilter("always")

            self.assertTrue(ctx.hash_needs_update(BAD1))
            self.assertFalse(ctx.hash_needs_update(GOOD1))
    
            if bcrypt.has_backend():            
                self.assertEquals(ctx.verify_and_update(PASS1,GOOD1), (True,None))
                self.assertEquals(ctx.verify_and_update("x",BAD1), (False,None))
                res = ctx.verify_and_update(PASS1, BAD1)
                self.assertTrue(res[0] and res[1] and res[1] != BAD1)
Exemplo n.º 3
0
class DatabaseLoginService:

    def __init__(self, session):
        self.db = session
        self.hasher = CryptContext(
            schemes=[
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
        )

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = (
                self.db.query(User.id)
                    .filter(User.username == username)
                    .one()
            )
        except NoResultFound:
            return

        return user.id

    def check_password(self, userid, password):
        user = self.get_user(userid)
        if user is None:
            return False

        # Actually check our hash, optionally getting a new hash for it if
        # we should upgrade our saved hashed.
        ok, new_hash = self.hasher.verify_and_update(password, user.password)

        # Check if the password itself was OK or not.
        if not ok:
            return False

        # If we've gotten a new password hash from the hasher, then we'll want
        # to save that hash.
        if new_hash:
            user.password = new_hash

        return True
Exemplo n.º 4
0
class DatabaseLoginService:
    def __init__(self, session):
        self.db = session
        self.hasher = CryptContext(
            schemes=[
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
        )

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = (self.db.query(
                User.id).filter(User.username == username).one())
        except NoResultFound:
            return

        return user.id

    def check_password(self, userid, password):
        user = self.get_user(userid)
        if user is None:
            return False

        # Actually check our hash, optionally getting a new hash for it if
        # we should upgrade our saved hashed.
        ok, new_hash = self.hasher.verify_and_update(password, user.password)

        # Check if the password itself was OK or not.
        if not ok:
            return False

        # If we've gotten a new password hash from the hasher, then we'll want
        # to save that hash.
        if new_hash:
            user.password = new_hash

        return True
Exemplo n.º 5
0
class DBAuth(auth.Auth, database.DatabaseHelper):
    plugin_name = "Local authentication"
    plugin_author = version.__author__
    plugin_license = version.__license__
    plugin_version = version.__version__
    plugin_copyright = version.__copyright__
    plugin_description = N_("Authentication management with local user / group database")
    plugin_database_branch = version.__branch__
    plugin_database_version = "0"

    def __init__(self, config):
        auth.Auth.__init__(self, config)
        database.DatabaseHelper.__init__(self)

    def init(self, config):
        adminuser = usergroup.User(config.get("initial_admin_user", usergroup.ADMIN_LOGIN))
        adminpass = config.get("initial_admin_pass", usergroup.ADMIN_LOGIN)
        deprecated_schemes = config.get("deprecated_password_schemes", "").split()
        schemes = config.get("password_schemes", "sha256_crypt").split() + deprecated_schemes

        try:
            self._context = CryptContext(schemes=schemes, deprecated=deprecated_schemes)
        except (KeyError, ValueError) as e:
            raise error.PrewikkaUserError(N_("DBAuth initialization error"), e)

        # If there are no accessible users other than ADMIN_LOGIN
        # with administrative rights, we grant all permissions to ADMIN_LOGIN.
        has_user_manager = any("USER_MANAGEMENT" in user.permissions and user != adminuser for user in self.get_user_list())
        if not has_user_manager:
            if not self.has_user(adminuser):
                adminuser.create()

            if not adminuser.has_property("password"):
                self.set_password(adminuser, adminpass)

            self.set_user_permissions(adminuser, usergroup.ALL_PERMISSIONS)

    def _verify(self, password, hashed):
        try:
            return self._context.verify_and_update(password, hashed)
        except (TypeError, ValueError):
            return False, None

    def can_create_user(self):
        return True

    def can_create_group(self):
        return True

    def can_delete_user(self):
        return True

    def can_delete_group(self):
        return True

    def get_user_list(self, search=None):
        query = "SELECT name, userid FROM Prewikka_User"
        if search:
            query += " WHERE name LIKE %s" % self.escape("%%%s%%" % search)
        return [usergroup.User(*r) for r in self.query(query)]

    def get_group_list(self, search=None):
        query = "SELECT name, groupid FROM Prewikka_Group"
        if search:
            query += " WHERE name LIKE %s" % self.escape("%%%s%%" % search)
        return [usergroup.Group(*r) for r in self.query(query)]

    def has_user(self, user):
        return self.get_user_by_id(user.id)

    def has_group(self, grp):
        return self.get_group_by_id(grp.id)

    def get_user_permissions(self, user, ignore_group=False):
        uid = self.escape(user.id)

        qstr = ""
        if not ignore_group:
            qstr = " UNION \
                     SELECT pgp.permission FROM Prewikka_User_Group pug \
                     JOIN Prewikka_Group_Permission pgp USING (groupid) \
                     WHERE pug.userid = %s" % (uid)

        return set(r[0] for r in self.query("SELECT pp.permission FROM Prewikka_User_Permission pp where pp.userid = %s%s" % (uid, qstr)))

    def get_group_permissions(self, group):
        return set(r[0] for r in self.query("SELECT permission FROM Prewikka_Group_Permission WHERE groupid = %s", group.id))

    def _set_permissions(self, table, field, obj, permissions):
        permissions = set(permissions)
        self.upsert(table, (field, "permission"), ((obj.id, perm) for perm in permissions), merge={field: obj.id})

    def set_user_permissions(self, user, permissions):
        self._set_permissions("Prewikka_User_Permission", "userid", user, permissions)

    def set_group_permissions(self, group, permissions):
        self._set_permissions("Prewikka_Group_Permission", "groupid", group, permissions)

    # Group specific
    def set_group_members(self, group, users):
        rows = ((group.id, user.id) for user in users)
        self.upsert("Prewikka_User_Group", ("groupid", "userid"), rows, merge={"groupid": group.id})

    def get_group_members(self, group):
        return [usergroup.User(*r) for r in self.query("SELECT PU.name, PUG.userid FROM Prewikka_User_Group PUG "
                                                       "JOIN Prewikka_User PU USING (userid) WHERE groupid = %s", group.id)]

    def set_member_of(self, user, groups):
        rows = ((group.id, user.id) for group in set(groups))
        self.upsert("Prewikka_User_Group", ("groupid", "userid"), rows, merge={"userid": user.id})

    def get_member_of(self, user):
        return [usergroup.Group(*r) for r in self.query("SELECT PG.name, PUG.groupid FROM Prewikka_User_Group PUG "
                                                        "JOIN Prewikka_Group PG USING (groupid) WHERE userid = %s", user.id)]

    def is_member_of(self, group, user):
        return bool(self.query("SELECT groupid from Prewikka_User_Group where groupid = %s and userid = %s", group.id, user.id))

    def get_user_permissions_from_groups(self, user):
        return set(r[0] for r in self.query("SELECT pgp.permission FROM Prewikka_User_Group pug \
                                             JOIN Prewikka_Group_Permission pgp USING (groupid) \
                                             WHERE pug.userid = %s ", user.id))

    def authenticate(self, login, password="", no_password_check=False):
        if login is None:
            raise auth.AuthError(env.session, _("No login name provided"))

        user = usergroup.User(login)
        if not self.has_user(user):
            raise auth.AuthError(env.session, log_user=login)

        if not no_password_check:
            try:
                real_password = user.get_property_fail("password")
            except:
                raise auth.AuthError(env.session, log_user=user)

            valid, new_hash = self._verify(password, real_password)
            if not valid:
                raise auth.AuthError(env.session, log_user=user)

            if new_hash:
                # Make sure the password uses the proper hashing algorithm
                user.set_property("password", new_hash)
                user.sync_properties()

        return user

    def set_password(self, user, password):
        user.set_property("password", self._context.hash(password))
        user.sync_properties()

    def get_default_session(self):
        return "loginform"
Exemplo n.º 6
0
class DatabaseUserService:

    def __init__(self, session, ratelimiters=None):
        if ratelimiters is None:
            ratelimiters = {}
        ratelimiters = collections.defaultdict(DummyRateLimiter, ratelimiters)

        self.db = session
        self.ratelimiters = ratelimiters
        self.hasher = CryptContext(
            schemes=[
                "argon2",
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
            truncate_error=True,

            # Argon 2 Configuration
            argon2__memory_cost=1024,
            argon2__parallelism=6,
            argon2__time_cost=6,
        )

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = (
                self.db.query(User.id)
                    .filter(User.username == username)
                    .one()
            )
        except NoResultFound:
            return

        return user.id

    @functools.lru_cache()
    def find_userid_by_email(self, email):
        try:
            # flake8: noqa
            user_id = (
                self.db.query(Email.user_id)
                    .filter(Email.email == email)
                    .one()
            )[0]
        except NoResultFound:
            return

        return user_id

    def check_password(self, userid, password):
        # The very first thing we want to do is check to see if we've hit our
        # global rate limit or not, assuming that we've been configured with a
        # global rate limiter anyways.
        if not self.ratelimiters["global"].test():
            logger.warning("Global failed login threshold reached.")
            raise TooManyFailedLogins(
                resets_in=self.ratelimiters["global"].resets_in(),
            )

        user = self.get_user(userid)
        if user is not None:
            # Now, check to make sure that we haven't hitten a rate limit on a
            # per user basis.
            if not self.ratelimiters["user"].test(user.id):
                raise TooManyFailedLogins(
                    resets_in=self.ratelimiters["user"].resets_in(user.id),
                )

            # Actually check our hash, optionally getting a new hash for it if
            # we should upgrade our saved hashed.
            ok, new_hash = self.hasher.verify_and_update(
                password,
                user.password,
            )

            # First, check to see if the password that we were given was OK.
            if ok:
                # Then, if the password was OK check to see if we've been given
                # a new password hash from the hasher, if so we'll want to save
                # that hash.
                if new_hash:
                    user.password = new_hash

                return True

        # If we've gotten here, then we'll want to record a failed login in our
        # rate limiting before returning False to indicate a failed password
        # verification.
        if user is not None:
            self.ratelimiters["user"].hit(user.id)
        self.ratelimiters["global"].hit()

        return False

    def create_user(self, username, name, password, email,
                    is_active=False, is_staff=False, is_superuser=False):

        user = User(username=username,
                    name=name,
                    password=self.hasher.hash(password),
                    is_active=is_active,
                    is_staff=is_staff,
                    is_superuser=is_superuser)
        self.db.add(user)
        email_object = Email(email=email, user=user,
                             primary=True, verified=False)
        self.db.add(email_object)
        # flush the db now so user.id is available
        self.db.flush()
        return user

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            setattr(user, attr, value)
        return user

    def verify_email(self, user_id, email_address):
        user = self.get_user(user_id)
        for email in user.emails:
            if email.email == email_address:
                email.verified = True
Exemplo n.º 7
0
class Praetorian:
    """
    Comprises the implementation for the flask-praetorian flask extension.
    Provides a tool that allows password authentication and token provision
    for applications and designated endpoints
    """
    def __init__(
        self,
        app=None,
        user_class=None,
        token_store_class=None,
        is_blacklisted=None,
        encode_jwt_token_hook=None,
        refresh_jwt_token_hook=None,
    ):
        self.pwd_ctx = None
        self.hash_scheme = None
        self.salt = None

        # TODO allow there to be only tokens if we want - not that important for now
        if app is not None and user_class is not None:
            self.init_app(
                app,
                user_class,
                token_store_class,
                is_blacklisted,
                encode_jwt_token_hook,
                refresh_jwt_token_hook,
            )

    def init_app(
        self,
        app=None,
        user_class=None,
        token_store_class=None,
        is_blacklisted=None,
        encode_jwt_token_hook=None,
        refresh_jwt_token_hook=None,
    ):
        """
        Initializes the Praetorian extension

        :param: app:                    The flask app to bind this
                                        extension to
        :param: user_class:             The class used to interact with
                                        user data
        :param: token_store_class:      The class used to interact with
                                        token store data
        :param: is_blacklisted:         A method that may optionally be
                                        used to check the token against
                                        a blacklist when access or refresh
                                        is requested should take the jti
                                        for the token to check as a single
                                        argument. Returns True if the jti is
                                        blacklisted, False otherwise. By
                                        default, always returns False.
        :param encode_jwt_token_hook:   A method that may optionally be
                                        called right before an encoded jwt
                                        is generated. Should take
                                        payload_parts which contains the
                                        ingredients for the jwt.
        :param refresh_jwt_token_hook:  A method that may optionally be called
                                        right before an encoded jwt is
                                        refreshed. Should take payload_parts
                                        which contains the ingredients for
                                        the jwt.
        """
        PraetorianError.require_condition(
            app.config.get('SECRET_KEY') is not None,
            "There must be a SECRET_KEY app config setting set",
        )

        self.roles_disabled = app.config.get(
            'PRAETORIAN_ROLES_DISABLED',
            DEFAULT_ROLES_DISABLED,
        )

        self.hash_autoupdate = app.config.get(
            'PRAETORIAN_HASH_AUTOUPDATE',
            DEFAULT_HASH_AUTOUPDATE,
        )

        self.hash_autotest = app.config.get(
            'PRAETORIAN_HASH_AUTOTEST',
            DEFAULT_HASH_AUTOTEST,
        )

        self.pwd_ctx = CryptContext(
            schemes=app.config.get(
                'PRAETORIAN_HASH_ALLOWED_SCHEMES',
                DEFAULT_HASH_ALLOWED_SCHEMES,
            ),
            default=app.config.get(
                'PRAETORIAN_HASH_SCHEME',
                DEFAULT_HASH_SCHEME,
            ),
            deprecated=app.config.get(
                'PRAETORIAN_HASH_DEPRECATED_SCHEMES',
                DEFAULT_HASH_DEPRECATED_SCHEMES,
            ),
        )

        valid_schemes = self.pwd_ctx.schemes()
        PraetorianError.require_condition(
            self.hash_scheme in valid_schemes or self.hash_scheme is None,
            "If {} is set, it must be one of the following schemes: {}".format(
                'PRAETORIAN_HASH_SCHEME',
                valid_schemes,
            ),
        )

        self.user_class = self._validate_user_class(user_class)
        #self.token_class = self._validate_token_class(token_class)
        self.token_store_class = token_store_class
        self.is_blacklisted = is_blacklisted or (lambda t: False)
        self.encode_jwt_token_hook = encode_jwt_token_hook
        self.refresh_jwt_token_hook = refresh_jwt_token_hook

        self.encode_key = app.config['SECRET_KEY']
        self.allowed_algorithms = app.config.get(
            'JWT_ALLOWED_ALGORITHMS',
            DEFAULT_JWT_ALLOWED_ALGORITHMS,
        )
        self.encode_algorithm = app.config.get(
            'JWT_ALGORITHM',
            DEFAULT_JWT_ALGORITHM,
        )
        self.access_lifespan = app.config.get(
            'JWT_ACCESS_LIFESPAN',
            DEFAULT_JWT_ACCESS_LIFESPAN,
        )
        self.refresh_lifespan = app.config.get(
            'JWT_REFRESH_LIFESPAN',
            DEFAULT_JWT_REFRESH_LIFESPAN,
        )
        self.reset_lifespan = app.config.get(
            'JWT_RESET_LIFESPAN',
            DEFAULT_JWT_RESET_LIFESPAN,
        )
        self.jwt_places = app.config.get(
            'JWT_PLACES',
            DEFAULT_JWT_PLACES,
        )
        self.cookie_name = app.config.get(
            'JWT_COOKIE_NAME',
            DEFAULT_JWT_COOKIE_NAME,
        )
        self.header_name = app.config.get(
            'JWT_HEADER_NAME',
            DEFAULT_JWT_HEADER_NAME,
        )
        self.header_type = app.config.get(
            'JWT_HEADER_TYPE',
            DEFAULT_JWT_HEADER_TYPE,
        )
        self.user_class_validation_method = app.config.get(
            'USER_CLASS_VALIDATION_METHOD',
            DEFAULT_USER_CLASS_VALIDATION_METHOD,
        )

        self.confirmation_template = app.config.get(
            'PRAETORIAN_CONFIRMATION_TEMPLATE',
            DEFAULT_CONFIRMATION_TEMPLATE,
        )
        self.confirmation_uri = app.config.get('PRAETORIAN_CONFIRMATION_URI', )
        self.confirmation_sender = app.config.get(
            'PRAETORIAN_CONFIRMATION_SENDER', )
        self.confirmation_subject = app.config.get(
            'PRAETORIAN_CONFIRMATION_SUBJECT',
            DEFAULT_CONFIRMATION_SUBJECT,
        )

        self.reset_template = app.config.get(
            'PRAETORIAN_RESET_TEMPLATE',
            DEFAULT_RESET_TEMPLATE,
        )
        self.reset_uri = app.config.get('PRAETORIAN_RESET_URI', )
        self.reset_sender = app.config.get('PRAETORIAN_RESET_SENDER', )
        self.reset_subject = app.config.get(
            'PRAETORIAN_RESET_SUBJECT',
            DEFAULT_RESET_SUBJECT,
        )

        if isinstance(self.access_lifespan, dict):
            self.access_lifespan = pendulum.duration(**self.access_lifespan)
        elif isinstance(self.access_lifespan, str):
            self.access_lifespan = duration_from_string(self.access_lifespan)
        ConfigurationError.require_condition(
            isinstance(self.access_lifespan, datetime.timedelta),
            "access lifespan was not configured",
        )

        if isinstance(self.refresh_lifespan, dict):
            self.refresh_lifespan = pendulum.duration(**self.refresh_lifespan)
        if isinstance(self.refresh_lifespan, str):
            self.refresh_lifespan = duration_from_string(self.refresh_lifespan)
        ConfigurationError.require_condition(
            isinstance(self.refresh_lifespan, datetime.timedelta),
            "refresh lifespan was not configured",
        )

        if not app.config.get('DISABLE_PRAETORIAN_ERROR_HANDLER'):
            app.register_error_handler(
                PraetorianError,
                PraetorianError.build_error_handler(),
            )

        self.is_testing = app.config.get('TESTING', False)

        if not hasattr(app, 'extensions'):
            app.extensions = {}
        app.extensions['praetorian'] = self

        return app

    def _validate_user_class(self, user_class):
        """
        Validates the supplied user_class to make sure that it has the
        class methods and attributes necessary to function correctly.
        After validating class methods, will attempt to instantiate a dummy
        instance of the user class to test for the requisite attributes

        Requirements:

        - ``lookup`` method. Accepts a string parameter, returns instance
        - ``identify`` method. Accepts an identity parameter, returns instance
        - ``identity`` attribute. Provides unique id for the instance
        - ``rolenames`` attribute. Provides list of roles attached to instance
        - ``password`` attribute. Provides hashed password for instance
        """
        PraetorianError.require_condition(
            getattr(user_class, 'lookup', None) is not None,
            textwrap.dedent("""
                The user_class must have a lookup class method:
                user_class.lookup(<str>) -> <user instance>
            """),
        )
        PraetorianError.require_condition(
            getattr(user_class, 'identify', None) is not None,
            textwrap.dedent("""
                The user_class must have an identify class method:
                user_class.identify(<identity>) -> <user instance>
            """),
        )

        dummy_user = None
        try:
            dummy_user = user_class()
        except Exception:
            flask.current_app.logger.debug(
                "Skipping instance validation because "
                "user cannot be instantiated without arguments")
        if dummy_user:
            PraetorianError.require_condition(
                hasattr(dummy_user, "identity"),
                textwrap.dedent("""
                    Instances of user_class must have an identity attribute:
                    user_instance.identity -> <unique id for instance>
                """),
            )
            PraetorianError.require_condition(
                self.roles_disabled or hasattr(dummy_user, "rolenames"),
                textwrap.dedent("""
                    Instances of user_class must have a rolenames attribute:
                    user_instance.rolenames -> [<role1>, <role2>, ...]
                """),
            )
            PraetorianError.require_condition(
                hasattr(dummy_user, "password"),
                textwrap.dedent("""
                    Instances of user_class must have a password attribute:
                    user_instance.rolenames -> <hashed password>
                """),
            )

        return user_class

    def _validate_token_class(self, token_store_class):
        """
        Validates the supplied user_class to make sure that it has the
        class methods and attributes necessary to function correctly.
        After validating class methods, will attempt to instantiate a dummy
        instance of the user class to test for the requisite attributes

        Requirements:

        - ``lookup`` method. Accepts a string parameter, returns instance
        - ``identify`` method. Accepts an identity parameter, returns instance
        - ``identity`` attribute. Provides unique id for the instance
        - ``rolenames`` attribute. Provides list of roles attached to instance
        - ``password`` attribute. Provides hashed password for instance  # TODO remove password
        """
        PraetorianError.require_condition(
            getattr(token_store_class, 'lookup', None) is not None,
            textwrap.dedent("""
                The user_class must have a lookup class method:
                user_class.lookup(<str>) -> <user instance>
            """),
        )
        PraetorianError.require_condition(
            getattr(token_store_class, 'identify', None) is not None,
            textwrap.dedent("""
                The user_class must have an identify class method:
                user_class.identify(<identity>) -> <user instance>
            """),
        )

        dummy_token_store = None
        try:
            dummy_token_store = token_store_class()
        except Exception:
            flask.current_app.logger.debug(
                "Skipping instance validation because "
                "token cannot be instantiated without arguments")
        if dummy_token_store:
            PraetorianError.require_condition(
                hasattr(dummy_token_store, "identity"),
                textwrap.dedent("""
                    Instances of token_class must have an identity attribute:
                    token_instance.identity -> <unique id for instance>
                """),
            )
            PraetorianError.require_condition(
                self.roles_disabled or hasattr(dummy_token_store, "rolenames"),
                textwrap.dedent("""
                    Instances of token_class must have a rolenames attribute:
                    token_instance.rolenames -> [<role1>, <role2>, ...]
                """),
            )

            # PraetorianError.require_condition(
            #     hasattr(dummy_token, "password"),
            #     textwrap.dedent("""
            #         Instances of user_class must have a password attribute:
            #         user_instance.rolenames -> <hashed password>
            #     """),
            # )

        return dummy_token_store

    def authenticate(self, username, password):
        """
        Verifies that a password matches the stored password for that username.
        If verification passes, the matching user instance is returned
        """
        PraetorianError.require_condition(
            self.user_class is not None,
            "Praetorian must be initialized before this method is available",
        )
        user = self.user_class.lookup(username)
        AuthenticationError.require_condition(
            user is not None and self._verify_password(
                password,
                user.password,
            ),
            'The username and/or password are incorrect',
        )
        """
        If we are set to PRAETORIAN_HASH_AUTOUPDATE then check our hash
            and if needed, update the user.  The developer is responsible
            for using the returned user object and updating the data
            storage endpoint.

        Else, if we are set to PRAETORIAN_HASH_AUTOTEST then check out hash
            and return exception if our hash is using the wrong scheme,
            but don't modify the user.
        """
        if self.hash_autoupdate:
            self.verify_and_update(user=user, password=password)
        elif self.hash_autotest:
            self.verify_and_update(user=user)

        return user

    def _verify_password(self, raw_password, hashed_password):
        """
        Verifies that a plaintext password matches the hashed version of that
        password using the stored passlib password context
        """
        PraetorianError.require_condition(
            self.pwd_ctx is not None,
            "Praetorian must be initialized before this method is available",
        )
        return self.pwd_ctx.verify(raw_password, hashed_password)

    @deprecated('Use `hash_password` instead.')
    def encrypt_password(self, raw_password):
        """
        *NOTE* This should be deprecated as its an incorrect definition for
            what is actually being done -- we are hashing, not encrypting
        """
        return self.hash_password(raw_password)

    def error_handler(self, error):
        """
        Provides a flask error handler that is used for PraetorianErrors
        (and derived exceptions).
        """
        warnings.warn(
            """
            error_handler is deprecated.
            Use FlaskBuzz.build_error_handler instead
            """,
            warnings.DeprecationWarning,
        )
        return error.jsonify(), error.status_code, error.headers

    def _check_user(self, user):
        """
        Checks to make sure that a user is valid. First, checks that the user
        is not None. If this check fails, a MissingUserError is raised. Next,
        checks if the user has a validation method. If the method does not
        exist, the check passes. If the method exists, it is called. If the
        result of the call is not truthy, an InvalidUserError is raised
        """
        MissingUserError.require_condition(
            user is not None,
            'Could not find the requested user',
        )
        user_validate_method = getattr(user, self.user_class_validation_method,
                                       None)
        if user_validate_method is None:
            return
        InvalidUserError.require_condition(
            user_validate_method(),
            "The user is not valid or has had access revoked",
        )

    def encode_jwt_token(self,
                         user,
                         override_access_lifespan=None,
                         override_refresh_lifespan=None,
                         bypass_user_check=False,
                         is_registration_token=False,
                         is_reset_token=False,
                         **custom_claims):
        """
        Encodes user data into a jwt token that can be used for authorization
        at protected endpoints

        :param: override_access_lifespan:  Override's the instance's access
                                           lifespan to set a custom duration
                                           after which the new token's
                                           accessability will expire. May not
                                           exceed the refresh_lifespan
        :param: override_refresh_lifespan: Override's the instance's refresh
                                           lifespan to set a custom duration
                                           after which the new token's
                                           refreshability will expire.
        :param: bypass_user_check:         Override checking the user for
                                           being real/active.  Used for
                                           registration token generation.
        :param: is_registration_token:     Indicates that the token will be
                                           used only for email-based
                                           registration
        :param: custom_claims:             Additional claims that should
                                           be packed in the payload. Note that
                                           any claims supplied here must be
                                           JSON compatible types
        """
        ClaimCollisionError.require_condition(
            set(custom_claims.keys()).isdisjoint(RESERVED_CLAIMS),
            "The custom claims collide with required claims",
        )
        if not bypass_user_check:
            self._check_user(user)

        moment = pendulum.now('UTC')

        if override_refresh_lifespan is None:
            refresh_lifespan = self.refresh_lifespan
        else:
            refresh_lifespan = override_refresh_lifespan
        refresh_expiration = (moment + refresh_lifespan).int_timestamp

        if override_access_lifespan is None:
            access_lifespan = self.access_lifespan
        else:
            access_lifespan = override_access_lifespan
        access_expiration = min(
            (moment + access_lifespan).int_timestamp,
            refresh_expiration,
        )

        payload_parts = {
            'iat': moment.int_timestamp,
            'exp': access_expiration,
            'jti': str(uuid.uuid4()),
            'id': user.identity,
            'rls': ','.join(user.rolenames),
            REFRESH_EXPIRATION_CLAIM: refresh_expiration,
        }
        if is_registration_token:
            payload_parts[IS_REGISTRATION_TOKEN_CLAIM] = True
        if is_reset_token:
            payload_parts[IS_RESET_TOKEN_CLAIM] = True
        flask.current_app.logger.debug(
            "Attaching custom claims: {}".format(custom_claims), )
        payload_parts.update(custom_claims)

        if self.encode_jwt_token_hook:
            self.encode_jwt_token_hook(**payload_parts)
        return jwt.encode(
            payload_parts,
            self.encode_key,
            self.encode_algorithm,
        ).decode('utf-8')

    def encode_eternal_jwt_token(self, user, **custom_claims):
        """
        This utility function encodes a jwt token that never expires

        .. note:: This should be used sparingly since the token could become
                  a security concern if it is ever lost. If you use this
                  method, you should be sure that your application also
                  implements a blacklist so that a given token can be blocked
                  should it be lost or become a security concern
        """
        return self.encode_jwt_token(user,
                                     override_access_lifespan=VITAM_AETERNUM,
                                     override_refresh_lifespan=VITAM_AETERNUM,
                                     **custom_claims)

    def refresh_jwt_token(self, token, override_access_lifespan=None):
        """
        Creates a new token for a user if and only if the old token's access
        permission is expired but its refresh permission is not yet expired.
        The new token's refresh expiration moment is the same as the old
        token's, but the new token's access expiration is refreshed

        :param: token:                     The existing jwt token that needs to
                                           be replaced with a new, refreshed
                                           token
        :param: override_access_lifespan:  Override's the instance's access
                                           lifespan to set a custom duration
                                           after which the new token's
                                           accessability will expire. May not
                                           exceed the refresh lifespan
        """
        moment = pendulum.now('UTC')
        data = self.extract_jwt_token(token, access_type=AccessType.refresh)

        user = self.user_class.identify(data['id'])
        self._check_user(user)

        if override_access_lifespan is None:
            access_lifespan = self.access_lifespan
        else:
            access_lifespan = override_access_lifespan
        refresh_expiration = data[REFRESH_EXPIRATION_CLAIM]
        access_expiration = min(
            (moment + access_lifespan).int_timestamp,
            refresh_expiration,
        )

        custom_claims = {
            k: v
            for (k, v) in data.items() if k not in RESERVED_CLAIMS
        }
        payload_parts = {
            'iat': moment.int_timestamp,
            'exp': access_expiration,
            'jti': data['jti'],
            'id': data['id'],
            'rls': ','.join(user.rolenames),
            REFRESH_EXPIRATION_CLAIM: refresh_expiration,
        }
        payload_parts.update(custom_claims)

        if self.refresh_jwt_token_hook:
            self.refresh_jwt_token_hook(**payload_parts)
        return jwt.encode(
            payload_parts,
            self.encode_key,
            self.encode_algorithm,
        ).decode('utf-8')

    def extract_jwt_token(self, token, access_type=AccessType.access):
        """
        Extracts a data dictionary from a jwt token
        """
        # Note: we disable exp verification because we will do it ourselves
        with InvalidTokenHeader.handle_errors('failed to decode JWT token'):
            data = jwt.decode(
                token,
                self.encode_key,
                algorithms=self.allowed_algorithms,
                options={'verify_exp': False},
            )
        self._validate_jwt_data(data, access_type=access_type)
        return data

    def _validate_jwt_data(self, data, access_type):
        """
        Validates that the data for a jwt token is valid
        """
        MissingClaimError.require_condition(
            'jti' in data,
            'Token is missing jti claim',
        )
        BlacklistedError.require_condition(
            not self.is_blacklisted(data['jti']),
            'Token has a blacklisted jti',
        )
        MissingClaimError.require_condition(
            'id' in data,
            'Token is missing id field',
        )
        MissingClaimError.require_condition(
            'exp' in data,
            'Token is missing exp claim',
        )
        MissingClaimError.require_condition(
            REFRESH_EXPIRATION_CLAIM in data,
            'Token is missing {} claim'.format(REFRESH_EXPIRATION_CLAIM),
        )
        moment = pendulum.now('UTC').int_timestamp
        if access_type == AccessType.access:
            MisusedRegistrationToken.require_condition(
                IS_REGISTRATION_TOKEN_CLAIM not in data,
                "registration token used for access")
            MisusedResetToken.require_condition(
                IS_RESET_TOKEN_CLAIM not in data,
                "password reset token used for access")
            ExpiredAccessError.require_condition(
                moment <= data['exp'],
                'access permission has expired',
            )
        elif access_type == AccessType.refresh:
            MisusedRegistrationToken.require_condition(
                IS_REGISTRATION_TOKEN_CLAIM not in data,
                "registration token used for refresh")
            MisusedResetToken.require_condition(
                IS_RESET_TOKEN_CLAIM not in data,
                "password reset token used for refresh")
            EarlyRefreshError.require_condition(
                moment > data['exp'],
                'access permission for token has not expired. may not refresh',
            )
            ExpiredRefreshError.require_condition(
                moment <= data[REFRESH_EXPIRATION_CLAIM],
                'refresh permission for token has expired',
            )
        elif access_type == AccessType.register:
            ExpiredAccessError.require_condition(
                moment <= data['exp'],
                'register permission has expired',
            )
            InvalidRegistrationToken.require_condition(
                IS_REGISTRATION_TOKEN_CLAIM in data,
                "invalid registration token used for verification")
            MisusedResetToken.require_condition(
                IS_RESET_TOKEN_CLAIM not in data,
                "password reset token used for registration")
        elif access_type == AccessType.reset:
            MisusedRegistrationToken.require_condition(
                IS_REGISTRATION_TOKEN_CLAIM not in data,
                "registration token used for reset")
            ExpiredAccessError.require_condition(
                moment <= data['exp'],
                'reset permission has expired',
            )
            InvalidResetToken.require_condition(
                IS_RESET_TOKEN_CLAIM in data,
                "invalid reset token used for verification")

    def _unpack_header(self, headers):
        """
        Unpacks a jwt token from a request header
        """
        jwt_header = headers.get(self.header_name)
        MissingToken.require_condition(
            jwt_header is not None,
            "JWT token not found in headers under '{}'".format(
                self.header_name, ),
        )

        match = re.match(self.header_type + r'\s*([\w\.-]+)', jwt_header)
        InvalidTokenHeader.require_condition(
            match is not None,
            "JWT header structure is invalid",
        )
        token = match.group(1)
        return token

    def read_token_from_header(self):
        """
        Unpacks a jwt token from the current flask request
        """
        return self._unpack_header(flask.request.headers)

    def _unpack_cookie(self, cookies):
        """
        Unpacks a jwt token from a request cookies
        """
        jwt_cookie = cookies.get(self.cookie_name)
        MissingToken.require_condition(
            jwt_cookie is not None,
            "JWT token not found in cookie under '{}'".format(
                self.cookie_name),
        )
        return jwt_cookie

    def read_token_from_cookie(self):
        """
        Unpacks a jwt token from the current flask request
        """
        return self._unpack_cookie(flask.request.cookies)

    def read_token(self):
        exc = None
        if 'header' in self.jwt_places:
            try:
                return self.read_token_from_header()
            except MissingToken as e:
                exc = e
        if 'cookie' in self.jwt_places:
            try:
                return self.read_token_from_cookie()
            except MissingToken as e:
                exc = e
        if exc:
            raise MissingToken("JWT token not found in {}".format(
                self.jwt_places))

    def pack_header_for_user(self,
                             user,
                             override_access_lifespan=None,
                             override_refresh_lifespan=None,
                             **custom_claims):
        """
        Encodes a jwt token and packages it into a header dict for a given user

        :param: user:                      The user to package the header for
        :param: override_access_lifespan:  Override's the instance's access
                                           lifespan to set a custom duration
                                           after which the new token's
                                           accessability will expire. May not
                                           exceed the refresh_lifespan
        :param: override_refresh_lifespan: Override's the instance's refresh
                                           lifespan to set a custom duration
                                           after which the new token's
                                           refreshability will expire.
        :param: custom_claims:             Additional claims that should
                                           be packed in the payload. Note that
                                           any claims supplied here must be
                                           JSON compatible types
        """
        token = self.encode_jwt_token(
            user,
            override_access_lifespan=override_access_lifespan,
            override_refresh_lifespan=override_refresh_lifespan,
            **custom_claims)
        return {self.header_name: self.header_type + ' ' + token}

    def send_registration_email(self,
                                email,
                                user=None,
                                template=None,
                                confirmation_sender=None,
                                confirmation_uri=None,
                                subject=None,
                                override_access_lifespan=None):
        """
        Sends a registration email to a new user, containing a time expiring
            token usable for validation.  This requires your application
            is initialized with a `mail` extension, which supports
            Flask-Mail's `Message()` object and a `send()` method.

        Returns a dict containing the information sent, along with the
            `result` from mail send.
        :param: user:                     The user object to tie claim to
                                          (username, id, email, etc)
        :param: template:                 HTML Template for confirmation email.
                                          If not provided, a stock entry is
                                          used
        :param: confirmation_sender:      The sender that shoudl be attached
                                          to the confirmation email. Overrides
                                          the PRAETORIAN_CONFIRMRATION_SENDER
                                          config setting
        :param: confirmation_uri:         The uri that should be visited to
                                          complete email registration. Should
                                          usually be a uri to a frontend or
                                          external service that calls a
                                          'finalize' method in the api to
                                          complete registration. Will override
                                          the PRAETORIAN_CONFIRMATION_URI
                                          config setting
        :param: subject:                  The registration email subject.
                                          Will override the
                                          PRAETORIAN_CONFIRMATION_SUBJECT
                                          config setting.
        :param: override_access_lifespan: Overrides the JWT_ACCESS_LIFESPAN
                                          to set an access lifespan for the
                                          registration token.
        """
        if subject is None:
            subject = self.confirmation_subject

        if confirmation_uri is None:
            confirmation_uri = self.confirmation_uri

        sender = confirmation_sender or self.confirmation_sender

        flask.current_app.logger.debug(
            "Generating token with lifespan: {}".format(
                override_access_lifespan))
        custom_token = self.encode_jwt_token(
            user,
            override_access_lifespan=override_access_lifespan,
            bypass_user_check=True,
            is_registration_token=True,
        )

        return self.send_token_email(email,
                                     user,
                                     template,
                                     confirmation_sender,
                                     confirmation_uri,
                                     subject,
                                     custom_token=custom_token,
                                     sender=sender)

    def send_reset_email(self,
                         email,
                         template=None,
                         reset_sender=None,
                         reset_uri=None,
                         subject=None,
                         override_access_lifespan=None):
        """
        Sends a password reset email to a user, containing a time expiring
            token usable for validation.  This requires your application
            is initialized with a `mail` extension, which supports
            Flask-Mail's `Message()` object and a `send()` method.

        Returns a dict containing the information sent, along with the
            `result` from mail send.
        :param: email:                    The email address to attempt to
                                          send to
        :param: template:                 HTML Template for reset email.
                                          If not provided, a stock entry is
                                          used
        :param: confirmation_sender:      The sender that shoudl be attached
                                          to the reset email. Overrides
                                          the PRAETORIAN_RESET_SENDER
                                          config setting
        :param: confirmation_uri:         The uri that should be visited to
                                          complete password reset. Should
                                          usually be a uri to a frontend or
                                          external service that calls the
                                          'validate_reset_token()' method in
                                          the api to complete reset. Will
                                          override the PRAETORIAN_RESET_URI
                                          config setting
        :param: subject:                  The reset email subject.
                                          Will override the
                                          PRAETORIAN_RESET_SUBJECT
                                          config setting.
        :param: override_access_lifespan: Overrides the JWT_ACCESS_LIFESPAN
                                          to set an access lifespan for the
                                          registration token.
        """
        if subject is None:
            subject = self.reset_subject

        if reset_uri is None:
            reset_uri = self.reset_uri

        sender = reset_sender or self.reset_sender

        user = self.user_class.lookup(email)
        MissingUserError.require_condition(
            user is not None,
            'Could not find the requested user',
        )

        flask.current_app.logger.debug(
            "Generating token with lifespan: {}".format(
                override_access_lifespan))
        custom_token = self.encode_jwt_token(
            user,
            override_access_lifespan=override_access_lifespan,
            bypass_user_check=False,
            is_reset_token=True,
        )

        return self.send_token_email(user.email,
                                     user,
                                     template,
                                     reset_sender,
                                     reset_uri,
                                     subject,
                                     custom_token=custom_token,
                                     sender=sender)

    def send_token_email(self,
                         email,
                         user=None,
                         template=None,
                         action_sender=None,
                         action_uri=None,
                         subject=None,
                         override_access_lifespan=None,
                         custom_token=None,
                         sender='no-reply@praetorian'):
        """
        Sends an email to a user, containing a time expiring
            token usable for several actions.  This requires
            your application is initialized with a `mail` extension,
            which supports Flask-Mail's `Message()` object and
            a `send()` method.

        Returns a dict containing the information sent, along with the
            `result` from mail send.
        :param: email:                    The email address to use
                                          (username, id, email, etc)
        :param: user:                     The user object to tie claim to
                                          (username, id, email, etc)
        :param: template:                 HTML Template for confirmation email.
                                          If not provided, a stock entry is
                                          used
        :param: action_sender:            The sender that should be attached
                                          to the confirmation email.
        :param: action_uri:               The uri that should be visited to
                                          complete the token action.
        :param: subject:                  The email subject.
        :param: override_access_lifespan: Overrides the JWT_ACCESS_LIFESPAN
                                          to set an access lifespan for the
                                          registration token.
        """
        notification = {
            'result': None,
            'message': None,
            'user': str(user),
            'email': email,
            'token': custom_token,
            'subject': subject,
            'confirmation_uri': action_uri,  # backwards compatibility
            'action_uri': action_uri,
        }

        PraetorianError.require_condition(
            action_sender,
            "A sender is required to send confirmation email",
        )

        PraetorianError.require_condition(
            custom_token,
            "A custom_token is required to send notification email",
        )

        if template is None:
            with open(self.confirmation_template) as fh:
                template = fh.read()

        with PraetorianError.handle_errors('fail sending email'):
            flask.current_app.logger.debug(
                "NOTIFICATION: {}".format(notification))

            jinja_tmpl = jinja2.Template(template)
            notification['message'] = jinja_tmpl.render(notification).strip()

            msg = Message(html=notification['message'],
                          sender=action_sender,
                          subject=notification['subject'],
                          recipients=[notification['email']])

            flask.current_app.logger.debug("Sending email to {}".format(email))
            notification['result'] = flask.current_app.extensions['mail'].send(
                msg)

        return notification

    def get_user_from_registration_token(self, token):
        """
        Gets a user based on the registration token that is supplied. Verifies
        that the token is a regisration token and that the user can be properly
        retrieved
        """
        data = self.extract_jwt_token(token, access_type=AccessType.register)
        flask.current_app.logger.debug("DATA: {}".format(data))
        user_id = data.get('id')
        PraetorianError.require_condition(
            user_id is not None,
            "Could not fetch an id from the registration token",
        )
        user = self.user_class.identify(user_id)
        PraetorianError.require_condition(
            user is not None,
            "Could not identify the user from the registration token",
        )
        return user

    def validate_reset_token(self, token):
        """
        Validates a password reset request based on the reset token
        that is supplied. Verifies that the token is a reset token
        and that the user can be properly retrieved
        """
        data = self.extract_jwt_token(token, access_type=AccessType.reset)
        user_id = data.get('id')
        PraetorianError.require_condition(
            user_id is not None,
            "Could not fetch an id from the reset token",
        )
        user = self.user_class.identify(user_id)
        PraetorianError.require_condition(
            user is not None,
            "Could not identify the user from the reset token",
        )
        return user

    def hash_password(self, raw_password):
        """
        Hashes a plaintext password using the stored passlib password context
        """
        PraetorianError.require_condition(
            self.pwd_ctx is not None,
            "Praetorian must be initialized before this method is available",
        )
        """
        `scheme` is now set with self.pwd_ctx.update(default=scheme) due
            to the depreciation in upcoming passlib 2.0.
         zillions of warnings suck.
        """
        return self.pwd_ctx.hash(raw_password)

    def verify_and_update(self, user=None, password=None):
        """
        Validate a password hash contained in the user object is
             hashed with the defined hash scheme (PRAETORIAN_HASH_SCHEME).
        If not, raise an Exception of `LegacySchema`, unless the
             `password` arguement is provided, in which case an attempt
             to call `user.save()` will be made, updating the hashed
             password to the currently desired hash scheme
             (PRAETORIAN_HASH_SCHEME).

        :param: user:     The user object to tie claim to
                              (username, id, email, etc). *MUST*
                              include the hashed password field,
                              defined as `user.password`
        :param: password: The user's provide password from login.
                          If present, this is used to validate
                              and then attempt to update with the
                              new PRAETORIAN_HASH_SCHEME scheme.
        """
        if self.pwd_ctx.needs_update(user.password):
            if password:
                (rv, updated) = self.pwd_ctx.verify_and_update(
                    password,
                    user.password,
                )
                AuthenticationError.require_condition(
                    rv,
                    "Could not verify password",
                )
                user.password = updated
            else:
                used_hash = self.pwd_ctx.identify(user.password)
                desired_hash = self.hash_scheme
                raise LegacyScheme("Hash using non-current scheme '{}'."
                                   "Use '{}' instead.".format(
                                       used_hash, desired_hash))

        return user
Exemplo n.º 8
0
class DatabaseUserService:
    def __init__(self, session):
        self.db = session
        self.hasher = CryptContext(
            schemes=[
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
        )

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = (self.db.query(
                User.id).filter(User.username == username).one())
        except NoResultFound:
            return

        return user.id

    def check_password(self, userid, password):
        user = self.get_user(userid)
        if user is None:
            return False

        # Actually check our hash, optionally getting a new hash for it if
        # we should upgrade our saved hashed.
        ok, new_hash = self.hasher.verify_and_update(password, user.password)

        # Check if the password itself was OK or not.
        if not ok:
            return False

        # If we've gotten a new password hash from the hasher, then we'll want
        # to save that hash.
        if new_hash:
            user.password = new_hash

        return True

    def create_user(self,
                    username,
                    name,
                    password,
                    email,
                    is_active=False,
                    is_staff=False,
                    is_superuser=False):
        user = User(username=username,
                    name=name,
                    password=password,
                    is_active=is_active,
                    is_staff=is_staff,
                    is_superuser=is_superuser)
        user.last_login = datetime.datetime.now()
        self.db.add(user)
        email_object = Email(email=email,
                             user=user,
                             primary=True,
                             verified=False)
        self.db.add(email_object)
        return user

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            setattr(user, attr, value)
        return user

    def verify_email(self, user_id, email_address):
        user = self.get_user(user_id)
        for email in user.emails:
            if email.email == email_address:
                email.verified = True
Exemplo n.º 9
0
class DatabaseUserService:
    def __init__(self, session, *, ratelimiters=None, metrics):
        if ratelimiters is None:
            ratelimiters = {}
        ratelimiters = collections.defaultdict(DummyRateLimiter, ratelimiters)

        self.db = session
        self.ratelimiters = ratelimiters
        self.hasher = CryptContext(
            schemes=[
                "argon2",
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
            truncate_error=True,
            # Argon 2 Configuration
            argon2__memory_cost=1024,
            argon2__parallelism=6,
            argon2__time_cost=6,
        )
        self._metrics = metrics

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def get_user_by_username(self, username):
        user_id = self.find_userid(username)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def get_user_by_email(self, email):
        user_id = self.find_userid_by_email(email)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = self.db.query(User.id).filter(User.username == username).one()
        except NoResultFound:
            return

        return user.id

    @functools.lru_cache()
    def find_userid_by_email(self, email):
        try:
            # flake8: noqa
            user_id = (self.db.query(Email.user_id).filter(Email.email == email).one())[
                0
            ]
        except NoResultFound:
            return

        return user_id

    def check_password(self, userid, password, *, tags=None):
        tags = tags if tags is not None else []

        self._metrics.increment("warehouse.authentication.start", tags=tags)

        # The very first thing we want to do is check to see if we've hit our
        # global rate limit or not, assuming that we've been configured with a
        # global rate limiter anyways.
        if not self.ratelimiters["global"].test():
            logger.warning("Global failed login threshold reached.")
            self._metrics.increment(
                "warehouse.authentication.ratelimited",
                tags=tags + ["ratelimiter:global"],
            )
            raise TooManyFailedLogins(resets_in=self.ratelimiters["global"].resets_in())

        user = self.get_user(userid)
        if user is not None:
            # Now, check to make sure that we haven't hitten a rate limit on a
            # per user basis.
            if not self.ratelimiters["user"].test(user.id):
                self._metrics.increment(
                    "warehouse.authentication.ratelimited",
                    tags=tags + ["ratelimiter:user"],
                )
                raise TooManyFailedLogins(
                    resets_in=self.ratelimiters["user"].resets_in(user.id)
                )

            # Actually check our hash, optionally getting a new hash for it if
            # we should upgrade our saved hashed.
            ok, new_hash = self.hasher.verify_and_update(password, user.password)

            # First, check to see if the password that we were given was OK.
            if ok:
                # Then, if the password was OK check to see if we've been given
                # a new password hash from the hasher, if so we'll want to save
                # that hash.
                if new_hash:
                    user.password = new_hash

                self._metrics.increment("warehouse.authentication.ok", tags=tags)

                return True
            else:
                self._metrics.increment(
                    "warehouse.authentication.failure",
                    tags=tags + ["failure_reason:password"],
                )
        else:
            self._metrics.increment(
                "warehouse.authentication.failure", tags=tags + ["failure_reason:user"]
            )

        # If we've gotten here, then we'll want to record a failed login in our
        # rate limiting before returning False to indicate a failed password
        # verification.
        if user is not None:
            self.ratelimiters["user"].hit(user.id)
        self.ratelimiters["global"].hit()

        return False

    def create_user(
        self, username, name, password, is_active=False, is_superuser=False
    ):

        user = User(
            username=username,
            name=name,
            password=self.hasher.hash(password),
            is_active=is_active,
            is_superuser=is_superuser,
        )
        self.db.add(user)
        self.db.flush()  # flush the db now so user.id is available

        return user

    def add_email(self, user_id, email_address, primary=None, verified=False):
        user = self.get_user(user_id)

        # If primary is None, then we're going to auto detect whether this should be the
        # primary address or not. The basic rule is that if the user doesn't already
        # have a primary address, then the address we're adding now is going to be
        # set to their primary.
        if primary is None:
            primary = True if user.primary_email is None else False

        email = Email(
            email=email_address, user=user, primary=primary, verified=verified
        )
        self.db.add(email)
        self.db.flush()  # flush the db now so email.id is available

        return email

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            if attr == PASSWORD_FIELD:
                value = self.hasher.hash(value)
            setattr(user, attr, value)

        # If we've given the user a new password, then we also want to unset the
        # reason for disable... because a new password means no more disabled
        # user.
        if PASSWORD_FIELD in changes:
            user.disabled_for = None

        return user

    def disable_password(self, user_id, reason=None):
        user = self.get_user(user_id)
        user.password = self.hasher.disable()
        user.disabled_for = reason

    def is_disabled(self, user_id):
        user = self.get_user(user_id)

        # User is not disabled.
        if self.hasher.is_enabled(user.password):
            return (False, None)
        # User is disabled.
        else:
            return (True, user.disabled_for)
Exemplo n.º 10
0
class Client(object):
    """ Authentication Client """
    def __init__(self):
        self.db = DBH(db='yubiauth')
        self.pwd_context = CryptContext(**settings['CRYPT_CONTEXT'])
        if settings['USE_NATIVE_YKVAL']:
            # Native verify
            from .ykval import Verifyer
            self.ykval_client = Verifyer()
        else:
            # Using yubico_client to verify against remote server
            from yubico_client import Yubico
            self.ykval_client = Yubico(settings['YKVAL_CLIENT_ID'],
                                       settings['YKVAL_CLIENT_SECRET'],
                                       api_urls=settings['YKVAL_SERVERS'])

    def _get_token_info(self, username, token_id):
        """
        1. Check user
        2. Check token
        """
        user = self.db.get_user(username)
        if not user:
            raise YKAuthError("No such user: %s" % username)
        logger.debug('Found user: %s', user)
        token = self.db.get_token(user['users_id'], token_id)
        if not token:
            logger.error('Token %s is not associated with %s', token_id, username)
            raise YKAuthError("Token %s is not associated with %s" % (token_id, username))
        logger.debug('Found token: %s', token)
        if not token.get('yubikeys_enabled'):
            logger.error('Token %s is disabled for %s', token_id, username)
            raise YKAuthError("Token is disabled for %s" % username)
        return user

    def _validate_password(self, user, password):
        """
        Validate password against the hash in SQL
        """
        valid, new_hash = self.pwd_context.verify_and_update(str(password), user['users_auth'])
        if not valid:
            logger.error('Invalid password for %(users_name)s', user)
            raise YKAuthError("Invalid password for %(users_name)s" % user)
        if new_hash:
            # TODO: update user's hash with new_hash
            logger.warning("User %(users_name)s's hash needs update", user)
        return True

    def _validate_otp(self, otp):
        """
        Use Yubico client to validate OTP
        """
        try:
            if self.ykval_client.verify(otp):
                return True
            return False
        except Exception as err:
            logger.error('OTP Validation failed: %r', err)
            return False

    def authenticate(self, username, password, otp):
        """
        1. Check if token is enabled
        2. Check if token is associated with the user
        3. Validate users password
        4. Validate OTP (YKVal)
        """
        token_id = otp[:-32]
        user = self._get_token_info(username, token_id)
        self._validate_password(user, password)
        return self._validate_otp(otp)
Exemplo n.º 11
0
class DatabaseUserService:
    def __init__(self, session, *, ratelimiters=None, remote_addr, metrics):
        if ratelimiters is None:
            ratelimiters = {}
        ratelimiters = collections.defaultdict(DummyRateLimiter, ratelimiters)

        self.db = session
        self.ratelimiters = ratelimiters
        self.hasher = CryptContext(
            schemes=[
                "argon2",
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
            truncate_error=True,
            # Argon 2 Configuration
            argon2__memory_cost=1024,
            argon2__parallelism=6,
            argon2__time_cost=6,
        )
        self.remote_addr = remote_addr
        self._metrics = metrics
        self.cached_get_user = functools.lru_cache()(self._get_user)

    def _get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).options(joinedload(User.webauthn)).get(userid)

    def get_user(self, userid):
        return self.cached_get_user(userid)

    @functools.lru_cache()
    def get_user_by_username(self, username):
        user_id = self.find_userid(username)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def get_user_by_email(self, email):
        user_id = self.find_userid_by_email(email)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def get_admins(self):
        return self.db.query(User).filter(User.is_superuser.is_(True)).all()

    def username_is_prohibited(self, username):
        return self.db.query(
            exists().where(ProhibitedUserName.name == username.lower())
        ).scalar()

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = self.db.query(User.id).filter(User.username == username).one()
        except NoResultFound:
            return

        return user.id

    @functools.lru_cache()
    def find_userid_by_email(self, email):
        try:
            user_id = (self.db.query(Email.user_id).filter(Email.email == email).one())[
                0
            ]
        except NoResultFound:
            return

        return user_id

    def _check_ratelimits(self, userid=None, tags=None):
        tags = tags if tags is not None else []

        # First we want to check if a single IP is exceeding our rate limiter.
        if self.remote_addr is not None:
            if not self.ratelimiters["ip.login"].test(self.remote_addr):
                logger.warning("IP failed login threshold reached.")
                self._metrics.increment(
                    "warehouse.authentication.ratelimited",
                    tags=tags + ["ratelimiter:ip"],
                )
                raise TooManyFailedLogins(
                    resets_in=self.ratelimiters["ip.login"].resets_in(self.remote_addr)
                )

        # Next check to see if we've hit our global rate limit or not,
        # assuming that we've been configured with a global rate limiter anyways.
        if not self.ratelimiters["global.login"].test():
            logger.warning("Global failed login threshold reached.")
            self._metrics.increment(
                "warehouse.authentication.ratelimited",
                tags=tags + ["ratelimiter:global"],
            )
            raise TooManyFailedLogins(
                resets_in=self.ratelimiters["global.login"].resets_in()
            )

        # Now, check to make sure that we haven't hitten a rate limit on a
        # per user basis.
        if userid is not None:
            if not self.ratelimiters["user.login"].test(userid):
                self._metrics.increment(
                    "warehouse.authentication.ratelimited",
                    tags=tags + ["ratelimiter:user"],
                )
                raise TooManyFailedLogins(
                    resets_in=self.ratelimiters["user.login"].resets_in(userid)
                )

    def _hit_ratelimits(self, userid=None):
        if userid is not None:
            self.ratelimiters["user.login"].hit(userid)
        self.ratelimiters["global.login"].hit()
        self.ratelimiters["ip.login"].hit(self.remote_addr)

    def check_password(self, userid, password, *, tags=None):
        tags = tags if tags is not None else []
        tags.append("mechanism:check_password")

        self._metrics.increment("warehouse.authentication.start", tags=tags)

        self._check_ratelimits(userid=None, tags=tags)

        user = self.get_user(userid)
        if user is not None:
            self._check_ratelimits(userid=user.id, tags=tags)

            # Actually check our hash, optionally getting a new hash for it if
            # we should upgrade our saved hashed.
            ok, new_hash = self.hasher.verify_and_update(password, user.password)

            # First, check to see if the password that we were given was OK.
            if ok:
                # Then, if the password was OK check to see if we've been given
                # a new password hash from the hasher, if so we'll want to save
                # that hash.
                if new_hash:
                    user.password = new_hash

                self._metrics.increment("warehouse.authentication.ok", tags=tags)

                return True
            else:
                self._metrics.increment(
                    "warehouse.authentication.failure",
                    tags=tags + ["failure_reason:password"],
                )
        else:
            self._metrics.increment(
                "warehouse.authentication.failure", tags=tags + ["failure_reason:user"]
            )

        # If we've gotten here, then we'll want to record a failed login in our
        # rate limiting before returning False to indicate a failed password
        # verification.
        self._hit_ratelimits(userid=(user.id if user is not None else None))
        return False

    def create_user(self, username, name, password):
        user = User(username=username, name=name, password=self.hasher.hash(password))
        self.db.add(user)
        self.db.flush()  # flush the db now so user.id is available

        return user

    def add_email(
        self,
        user_id,
        email_address,
        primary=None,
        verified=False,
        public=False,
    ):
        # Check to make sure that we haven't hitten the rate limit for this IP
        if not self.ratelimiters["email.add"].test(self.remote_addr):
            self._metrics.increment(
                "warehouse.email.add.ratelimited", tags=["ratelimiter:email.add"]
            )
            raise TooManyEmailsAdded(
                resets_in=self.ratelimiters["email.add"].resets_in(self.remote_addr)
            )

        user = self.get_user(user_id)

        # If primary is None, then we're going to auto detect whether this should be the
        # primary address or not. The basic rule is that if the user doesn't already
        # have a primary address, then the address we're adding now is going to be
        # set to their primary.
        if primary is None:
            primary = True if user.primary_email is None else False

        email = Email(
            email=email_address,
            user=user,
            primary=primary,
            verified=verified,
            public=public,
        )
        self.db.add(email)
        self.db.flush()  # flush the db now so email.id is available

        self.ratelimiters["email.add"].hit(self.remote_addr)
        self._metrics.increment("warehouse.email.add.ok")

        return email

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            if attr == PASSWORD_FIELD:
                value = self.hasher.hash(value)
            setattr(user, attr, value)

        # If we've given the user a new password, then we also want to unset the
        # reason for disable... because a new password means no more disabled
        # user.
        if PASSWORD_FIELD in changes:
            user.disabled_for = None

        return user

    def disable_password(self, user_id, reason=None):
        user = self.get_user(user_id)
        user.password = self.hasher.disable()
        user.disabled_for = reason

    def is_disabled(self, user_id):
        user = self.get_user(user_id)

        if user.is_frozen:
            return (True, DisableReason.AccountFrozen)

        # User is disabled due to password being disabled
        if not self.hasher.is_enabled(user.password):
            return (True, user.disabled_for)

        # User is not disabled.
        return (False, None)

    def has_two_factor(self, user_id):
        """
        Returns True if the user has any form of two factor
        authentication and is allowed to use it.
        """
        user = self.get_user(user_id)

        return user.has_two_factor

    def has_totp(self, user_id):
        """
        Returns True if the user has a TOTP device provisioned.
        """
        user = self.get_user(user_id)

        return user.totp_secret is not None

    def has_webauthn(self, user_id):
        """
        Returns True if the user has a security key provisioned.
        """
        user = self.get_user(user_id)

        return len(user.webauthn) > 0

    def has_recovery_codes(self, user_id):
        """
        Returns True if the user has generated recovery codes.
        """
        user = self.get_user(user_id)

        return user.has_recovery_codes

    def get_recovery_codes(self, user_id):
        """
        Returns all recovery codes for the user
        """
        user = self.get_user(user_id)

        stored_recovery_codes = self.db.query(RecoveryCode).filter_by(user=user).all()

        if stored_recovery_codes:
            return stored_recovery_codes

        self._metrics.increment(
            "warehouse.authentication.recovery_code.failure",
            tags=["failure_reason:no_recovery_codes"],
        )
        # If we've gotten here, then we'll want to record a failed attempt in our
        # rate limiting before raising an exception to indicate a failed
        # recovery code verification.
        self._hit_ratelimits(userid=user_id)
        raise NoRecoveryCodes

    def get_recovery_code(self, user_id, code):
        """
        Returns a specific recovery code if it exists
        """
        user = self.get_user(user_id)

        for stored_recovery_code in self.get_recovery_codes(user.id):
            if self.hasher.verify(code, stored_recovery_code.code):
                return stored_recovery_code

        self._metrics.increment(
            "warehouse.authentication.recovery_code.failure",
            tags=["failure_reason:invalid_recovery_code"],
        )
        # If we've gotten here, then we'll want to record a failed attempt in our
        # rate limiting before returning False to indicate a failed recovery code
        # verification.
        self._hit_ratelimits(userid=user_id)
        raise InvalidRecoveryCode

    def get_totp_secret(self, user_id):
        """
        Returns the user's TOTP secret as bytes.

        If the user doesn't have a TOTP, returns None.
        """
        user = self.get_user(user_id)

        return user.totp_secret

    def get_last_totp_value(self, user_id):
        """
        Returns the user's last (accepted) TOTP value.

        If the user doesn't have a TOTP or hasn't used their TOTP
        method, returns None.
        """
        user = self.get_user(user_id)

        return user.last_totp_value

    def check_totp_value(self, user_id, totp_value, *, tags=None):
        """
        Returns True if the given TOTP is valid against the user's secret.

        If the user doesn't have a TOTP secret or isn't allowed
        to use second factor methods, returns False.
        """
        tags = tags if tags is not None else []
        tags.append("mechanism:check_totp_value")
        self._metrics.increment("warehouse.authentication.two_factor.start", tags=tags)

        self._check_ratelimits(userid=user_id, tags=tags)

        totp_secret = self.get_totp_secret(user_id)

        if totp_secret is None:
            self._metrics.increment(
                "warehouse.authentication.two_factor.failure",
                tags=tags + ["failure_reason:no_totp"],
            )
            # If we've gotten here, then we'll want to record a failed attempt in our
            # rate limiting before returning False to indicate a failed totp
            # verification.
            self._hit_ratelimits(userid=user_id)
            return False

        last_totp_value = self.get_last_totp_value(user_id)

        if last_totp_value is not None and totp_value == last_totp_value.encode():
            return False

        valid = otp.verify_totp(totp_secret, totp_value)

        if valid:
            self._metrics.increment("warehouse.authentication.two_factor.ok", tags=tags)
        else:
            self._metrics.increment(
                "warehouse.authentication.two_factor.failure",
                tags=tags + ["failure_reason:invalid_totp"],
            )
            # If we've gotten here, then we'll want to record a failed attempt in our
            # rate limiting before returning False to indicate a failed totp
            # verification.
            self._hit_ratelimits(userid=user_id)

        return valid

    def get_webauthn_credential_options(self, user_id, *, challenge, rp_name, rp_id):
        """
        Returns a dictionary of credential options suitable for beginning the WebAuthn
        provisioning process for the given user.
        """
        user = self.get_user(user_id)

        return webauthn.get_credential_options(
            user, challenge=challenge, rp_name=rp_name, rp_id=rp_id
        )

    def get_webauthn_assertion_options(self, user_id, *, challenge, rp_id):
        """
        Returns a dictionary of assertion options suitable for beginning the WebAuthn
        authentication process for the given user.
        """
        user = self.get_user(user_id)

        return webauthn.get_assertion_options(user, challenge=challenge, rp_id=rp_id)

    def verify_webauthn_credential(self, credential, *, challenge, rp_id, origin):
        """
        Checks whether the given credential is valid, i.e. suitable for generating
        assertions during authentication.

        Returns the validated credential on success, raises
        webauthn.RegistrationRejectedError on failure.
        """
        validated_credential = webauthn.verify_registration_response(
            credential, challenge=challenge, rp_id=rp_id, origin=origin
        )

        webauthn_cred = (
            self.db.query(WebAuthn)
            .filter_by(
                credential_id=bytes_to_base64url(validated_credential.credential_id)
            )
            .first()
        )

        if webauthn_cred is not None:
            raise webauthn.RegistrationRejectedError("Credential ID already in use")

        return validated_credential

    def verify_webauthn_assertion(
        self, user_id, assertion, *, challenge, origin, rp_id
    ):
        """
        Checks whether the given assertion was produced by the given user's WebAuthn
        device.

        Returns the updated signage count on success, raises
        webauthn.AuthenticationRejectedError on failure.
        """
        user = self.get_user(user_id)

        return webauthn.verify_assertion_response(
            assertion, challenge=challenge, user=user, origin=origin, rp_id=rp_id
        )

    def add_webauthn(self, user_id, **kwargs):
        """
        Adds a WebAuthn credential to the given user.

        Returns None if the user already has this credential.
        """
        user = self.get_user(user_id)

        webauthn = WebAuthn(user=user, **kwargs)
        self.db.add(webauthn)
        self.db.flush()  # flush the db now so webauthn.id is available

        return webauthn

    def get_webauthn_by_label(self, user_id, label):
        """
        Returns a WebAuthn credential for the given user by its label,
        or None if no credential for the user has this label.
        """
        user = self.get_user(user_id)

        return next(
            (credential for credential in user.webauthn if credential.label == label),
            None,
        )

    def get_webauthn_by_credential_id(self, user_id, credential_id):
        """
        Returns a WebAuthn credential for the given user by its credential ID,
        or None of the user doesn't have a credential with this ID.
        """
        user = self.get_user(user_id)

        return next(
            (
                credential
                for credential in user.webauthn
                if credential.credential_id == credential_id
            ),
            None,
        )

    def record_event(self, user_id, *, tag, additional=None):
        """
        Creates a new UserEvent for the given user with the given
        tag, IP address, and additional metadata.

        Returns the event.
        """
        user = self.get_user(user_id)
        return user.record_event(
            tag=tag, ip_address=self.remote_addr, additional=additional
        )

    def generate_recovery_codes(self, user_id):
        user = self.get_user(user_id)

        if user.has_recovery_codes:
            self.db.query(RecoveryCode).filter_by(user=user).delete()

        recovery_codes = [secrets.token_hex(8) for _ in range(RECOVERY_CODE_COUNT)]
        for recovery_code in recovery_codes:
            self.db.add(RecoveryCode(user=user, code=self.hasher.hash(recovery_code)))

        self.db.flush()

        return recovery_codes

    def check_recovery_code(self, user_id, code):
        self._metrics.increment("warehouse.authentication.recovery_code.start")

        self._check_ratelimits(
            userid=user_id,
            tags=["mechanism:check_recovery_code"],
        )

        user = self.get_user(user_id)
        stored_recovery_code = self.get_recovery_code(user.id, code)

        if stored_recovery_code.burned:
            self._metrics.increment(
                "warehouse.authentication.recovery_code.failure",
                tags=["failure_reason:burned_recovery_code"],
            )
            raise BurnedRecoveryCode

        # The code is valid and not burned. Mark it as burned
        stored_recovery_code.burned = datetime.datetime.now()
        self.db.flush()
        self._metrics.increment("warehouse.authentication.recovery_code.ok")
        return True

    def get_password_timestamp(self, user_id):
        user = self.get_user(user_id)
        return user.password_date.timestamp() if user.password_date is not None else 0
Exemplo n.º 12
0
class Client:
    """ Authentication Client """
    def __init__(self):
        self.db = DBHandler(db='yubiauth')
        self.pwd_context = CryptContext(**settings['CRYPT_CONTEXT'])
        if settings['USE_NATIVE_YKVAL']:
            # Native verify
            from .ykval import Validator
            self.ykval_client = Validator()
        else:
            # Using yubico_client to verify against remote server
            from yubico_client import Yubico
            self.ykval_client = Yubico(settings['YKVAL_CLIENT_ID'],
                                       settings['YKVAL_CLIENT_SECRET'],
                                       api_urls=settings['YKVAL_SERVERS'])

    def _get_user_info(self, username):
        """
        Get user from DB

        Args:
            username

        Returns:
            dictionary of user data

        Raises:
            AuthFail if user does not exist
        """
        user = self.db.get_user(username)
        if not user:
            raise YKAuthError('UNKNOWN_USER')
        logger.debug('[%s] Found user: %s', username, user)
        return user

    def _check_token(self, user, token_id):
        """
        Check Token association with user

        Args:
            user: User data dict as recieved from _get_user_info()
            token_id: Token prefix (aka. publicname)

        Returns:
            None

        Raises:
            AuthFail if token is not associated with the user
            AithFail if token is disabled
        """
        token = self.db.get_token(user['users_id'], token_id)
        if not token:
            logger.error('[%s] Token %s is not associated with user',
                         user['users_name'], token_id)
            raise YKAuthError('INVALID_TOKEN')
        logger.debug('[%s] Found token: %s', user['users_name'], token)
        if not token.get('yubikeys_enabled'):
            logger.error('[%s] Token %s is disabled for %s',
                         user['users_name'], token_id, user['users_name'])
            raise YKAuthError('DISABLED_TOKEN')

    def _validate_password(self, user, password):
        """
        Validate password against the hash in SQL
        """
        valid, new_hash = self.pwd_context.verify_and_update(
            str(password), user['users_auth'])
        if not valid:
            logger.error('[%(users_name)s] Invalid password', user)
            raise YKAuthError('BAD_PASSWORD')
        if new_hash:
            # TODO: update user's hash with new_hash
            logger.warning('[%(users_name)s] User password hash needs update',
                           user)
        return True

    def authenticate(self, username, password, otp):
        """
        Yubistack user authentication

        Args:
            username: Username of the user
            password: Password/PIN of the user
            otp: Yubikey one time password

        Returns:
            dict of authentication data

        Authentication process:
            1. Check if token is enabled
            2. Check if token is associated with the user & enabled
            3. Validate users password
            4. Validate OTP (YKVal)
        """
        token_id = otp[:-TOKEN_LEN]
        # STEP 1: Check if token is enabled
        user = self._get_user_info(username)
        # STEP 2: Check if token is associated with the user & enabled
        self._check_token(user, token_id)
        # STEP 3: Validate users password
        self._validate_password(user, password)
        # STEP 4: Validate OTP
        self.ykval_client.verify(otp)
        return True
Exemplo n.º 13
0
class Client:
    """ Authentication Client """
    def __init__(self):
        self.db = DBHandler(db='yubiauth')
        self.pwd_context = CryptContext(**settings['CRYPT_CONTEXT'])
        if settings['USE_NATIVE_YKVAL']:
            # Native verify
            from .ykval import Validator
            self.ykval_client = Validator()
        else:
            # Using yubico_client to verify against remote server
            from yubico_client import Yubico
            self.ykval_client = Yubico(settings['YKVAL_CLIENT_ID'],
                                       settings['YKVAL_CLIENT_SECRET'],
                                       api_urls=settings['YKVAL_SERVERS'])

    def _get_user_info(self, username):
        """
        Get user from DB

        Args:
            username

        Returns:
            dictionary of user data

        Raises:
            AuthFail if user does not exist
        """
        user = self.db.get_user(username)
        if not user:
            raise YKAuthError('UNKNOWN_USER')
        logger.debug('[%s] Found user: %s', username, user)
        return user

    def _check_token(self, user, token_id):
        """
        Check Token association with user

        Args:
            user: User data dict as recieved from _get_user_info()
            token_id: Token prefix (aka. publicname)

        Returns:
            None

        Raises:
            AuthFail if token is not associated with the user
            AithFail if token is disabled
        """
        token = self.db.get_token(user['users_id'], token_id)
        if not token:
            logger.error('[%s] Token %s is not associated with user',
                         user['users_name'], token_id)
            raise YKAuthError('INVALID_TOKEN')
        logger.debug('[%s] Found token: %s', user['users_name'], token)
        if not token.get('yubikeys_enabled'):
            logger.error('[%s] Token %s is disabled for %s',
                         user['users_name'], token_id, user['users_name'])
            raise YKAuthError('DISABLED_TOKEN')

    def _validate_password(self, user, password):
        """
        Validate password against the hash in SQL
        """
        valid, new_hash = self.pwd_context.verify_and_update(str(password), user['users_auth'])
        if not valid:
            logger.error('[%(users_name)s] Invalid password', user)
            raise YKAuthError('BAD_PASSWORD')
        if new_hash:
            # TODO: update user's hash with new_hash
            logger.warning('[%(users_name)s] User password hash needs update', user)
        return True

    def authenticate(self, username, password, otp):
        """
        Yubistack user authentication

        Args:
            username: Username of the user
            password: Password/PIN of the user
            otp: Yubikey one time password

        Returns:
            dict of authentication data

        Authentication process:
            1. Check if token is enabled
            2. Check if token is associated with the user & enabled
            3. Validate users password
            4. Validate OTP (YKVal)
        """
        token_id = otp[:-TOKEN_LEN]
        # STEP 1: Check if token is enabled
        user = self._get_user_info(username)
        # STEP 2: Check if token is associated with the user & enabled
        self._check_token(user, token_id)
        # STEP 3: Validate users password
        self._validate_password(user, password)
        # STEP 4: Validate OTP
        self.ykval_client.verify(otp)
        return True
Exemplo n.º 14
0
class DatabaseUserService:

    def __init__(self, session):
        self.db = session
        self.hasher = CryptContext(
            schemes=[
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
        )

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = (
                self.db.query(User.id)
                    .filter(User.username == username)
                    .one()
            )
        except NoResultFound:
            return

        return user.id

    @functools.lru_cache()
    def find_userid_by_email(self, email):
        try:
            # flake8: noqa
            user_id = (
                self.db.query(Email.user_id)
                    .filter(Email.email == email)
                    .one()
            )[0]
        except NoResultFound:
            return

        return user_id

    def check_password(self, userid, password):
        user = self.get_user(userid)
        if user is None:
            return False

        # Actually check our hash, optionally getting a new hash for it if
        # we should upgrade our saved hashed.
        ok, new_hash = self.hasher.verify_and_update(password, user.password)

        # Check if the password itself was OK or not.
        if not ok:
            return False

        # If we've gotten a new password hash from the hasher, then we'll want
        # to save that hash.
        if new_hash:
            user.password = new_hash

        return True

    def create_user(self, username, name, password, email,
                    is_active=False, is_staff=False, is_superuser=False):

        user = User(username=username,
                    name=name,
                    password=self.hasher.encrypt(password),
                    is_active=is_active,
                    is_staff=is_staff,
                    is_superuser=is_superuser)
        user.last_login = datetime.datetime.now()
        self.db.add(user)
        email_object = Email(email=email, user=user,
                             primary=True, verified=False)
        self.db.add(email_object)
        # flush the db now so user.id is available
        self.db.flush()
        return user

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            setattr(user, attr, value)
        return user

    def verify_email(self, user_id, email_address):
        user = self.get_user(user_id)
        for email in user.emails:
            if email.email == email_address:
                email.verified = True
Exemplo n.º 15
0
class DatabaseUserService:
    def __init__(self, session, *, ratelimiters=None, metrics):
        if ratelimiters is None:
            ratelimiters = {}
        ratelimiters = collections.defaultdict(DummyRateLimiter, ratelimiters)

        self.db = session
        self.ratelimiters = ratelimiters
        self.hasher = CryptContext(
            schemes=[
                "argon2",
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
            truncate_error=True,
            # Argon 2 Configuration
            argon2__memory_cost=1024,
            argon2__parallelism=6,
            argon2__time_cost=6,
        )
        self._metrics = metrics

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def get_user_by_username(self, username):
        user_id = self.find_userid(username)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def get_user_by_email(self, email):
        user_id = self.find_userid_by_email(email)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = self.db.query(
                User.id).filter(User.username == username).one()
        except NoResultFound:
            return

        return user.id

    @functools.lru_cache()
    def find_userid_by_email(self, email):
        try:
            # flake8: noqa
            user_id = (self.db.query(
                Email.user_id).filter(Email.email == email).one())[0]
        except NoResultFound:
            return

        return user_id

    def check_password(self, userid, password, *, tags=None):
        tags = tags if tags is not None else []

        self._metrics.increment("warehouse.authentication.start", tags=tags)

        # The very first thing we want to do is check to see if we've hit our
        # global rate limit or not, assuming that we've been configured with a
        # global rate limiter anyways.
        if not self.ratelimiters["global"].test():
            logger.warning("Global failed login threshold reached.")
            self._metrics.increment(
                "warehouse.authentication.ratelimited",
                tags=tags + ["ratelimiter:global"],
            )
            raise TooManyFailedLogins(
                resets_in=self.ratelimiters["global"].resets_in())

        user = self.get_user(userid)
        if user is not None:
            # Now, check to make sure that we haven't hitten a rate limit on a
            # per user basis.
            if not self.ratelimiters["user"].test(user.id):
                self._metrics.increment(
                    "warehouse.authentication.ratelimited",
                    tags=tags + ["ratelimiter:user"],
                )
                raise TooManyFailedLogins(
                    resets_in=self.ratelimiters["user"].resets_in(user.id))

            # Actually check our hash, optionally getting a new hash for it if
            # we should upgrade our saved hashed.
            ok, new_hash = self.hasher.verify_and_update(
                password, user.password)

            # First, check to see if the password that we were given was OK.
            if ok:
                # Then, if the password was OK check to see if we've been given
                # a new password hash from the hasher, if so we'll want to save
                # that hash.
                if new_hash:
                    user.password = new_hash

                self._metrics.increment("warehouse.authentication.ok",
                                        tags=tags)

                return True
            else:
                self._metrics.increment(
                    "warehouse.authentication.failure",
                    tags=tags + ["failure_reason:password"],
                )
        else:
            self._metrics.increment("warehouse.authentication.failure",
                                    tags=tags + ["failure_reason:user"])

        # If we've gotten here, then we'll want to record a failed login in our
        # rate limiting before returning False to indicate a failed password
        # verification.
        if user is not None:
            self.ratelimiters["user"].hit(user.id)
        self.ratelimiters["global"].hit()

        return False

    def create_user(self, username, name, password):
        user = User(username=username,
                    name=name,
                    password=self.hasher.hash(password))
        self.db.add(user)
        self.db.flush()  # flush the db now so user.id is available

        return user

    def add_email(self, user_id, email_address, primary=None, verified=False):
        user = self.get_user(user_id)

        # If primary is None, then we're going to auto detect whether this should be the
        # primary address or not. The basic rule is that if the user doesn't already
        # have a primary address, then the address we're adding now is going to be
        # set to their primary.
        if primary is None:
            primary = True if user.primary_email is None else False

        email = Email(email=email_address,
                      user=user,
                      primary=primary,
                      verified=verified)
        self.db.add(email)
        self.db.flush()  # flush the db now so email.id is available

        return email

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            if attr == PASSWORD_FIELD:
                value = self.hasher.hash(value)
            setattr(user, attr, value)

        # If we've given the user a new password, then we also want to unset the
        # reason for disable... because a new password means no more disabled
        # user.
        if PASSWORD_FIELD in changes:
            user.disabled_for = None

        return user

    def disable_password(self, user_id, reason=None):
        user = self.get_user(user_id)
        user.password = self.hasher.disable()
        user.disabled_for = reason

    def is_disabled(self, user_id):
        user = self.get_user(user_id)

        # User is not disabled.
        if self.hasher.is_enabled(user.password):
            return (False, None)
        # User is disabled.
        else:
            return (True, user.disabled_for)

    def has_two_factor(self, user_id):
        """
        Returns True if the user has any form of two factor
        authentication and is allowed to use it.
        """
        user = self.get_user(user_id)

        return user.has_two_factor

    def has_totp(self, user_id):
        """
        Returns True if the user has a TOTP device provisioned.
        """
        user = self.get_user(user_id)

        return user.totp_secret is not None

    def has_webauthn(self, user_id):
        """
        Returns True if the user has a security key provisioned.
        """
        user = self.get_user(user_id)

        return len(user.webauthn) > 0

    def get_totp_secret(self, user_id):
        """
        Returns the user's TOTP secret as bytes.

        If the user doesn't have a TOTP, returns None.
        """
        user = self.get_user(user_id)

        return user.totp_secret

    def check_totp_value(self, user_id, totp_value, *, tags=None):
        """
        Returns True if the given TOTP is valid against the user's secret.

        If the user doesn't have a TOTP secret or isn't allowed
        to use second factor methods, returns False.
        """
        tags = tags if tags is not None else []
        self._metrics.increment("warehouse.authentication.two_factor.start",
                                tags=tags)

        # The very first thing we want to do is check to see if we've hit our
        # global rate limit or not, assuming that we've been configured with a
        # global rate limiter anyways.
        if not self.ratelimiters["global"].test():
            logger.warning("Global failed login threshold reached.")
            self._metrics.increment(
                "warehouse.authentication.two_factor.ratelimited",
                tags=tags + ["ratelimiter:global"],
            )
            raise TooManyFailedLogins(
                resets_in=self.ratelimiters["global"].resets_in())

        # Now, check to make sure that we haven't hitten a rate limit on a
        # per user basis.
        if not self.ratelimiters["user"].test(user_id):
            self._metrics.increment(
                "warehouse.authentication.two_factor.ratelimited",
                tags=tags + ["ratelimiter:user"],
            )
            raise TooManyFailedLogins(
                resets_in=self.ratelimiters["user"].resets_in(user_id))

        totp_secret = self.get_totp_secret(user_id)

        if totp_secret is None:
            self._metrics.increment(
                "warehouse.authentication.two_factor.failure",
                tags=tags + ["failure_reason:no_totp"],
            )
            # If we've gotten here, then we'll want to record a failed attempt in our
            # rate limiting before returning False to indicate a failed totp
            # verification.
            self.ratelimiters["user"].hit(user_id)
            self.ratelimiters["global"].hit()
            return False

        valid = otp.verify_totp(totp_secret, totp_value)

        if valid:
            self._metrics.increment("warehouse.authentication.two_factor.ok",
                                    tags=tags)
        else:
            self._metrics.increment(
                "warehouse.authentication.two_factor.failure",
                tags=tags + ["failure_reason:invalid_totp"],
            )
            # If we've gotten here, then we'll want to record a failed attempt in our
            # rate limiting before returning False to indicate a failed totp
            # verification.
            self.ratelimiters["user"].hit(user_id)
            self.ratelimiters["global"].hit()

        return valid

    def get_webauthn_credential_options(self, user_id, *, challenge, rp_name,
                                        rp_id):
        """
        Returns a dictionary of credential options suitable for beginning the WebAuthn
        provisioning process for the given user.
        """
        user = self.get_user(user_id)

        return webauthn.get_credential_options(user,
                                               challenge=challenge,
                                               rp_name=rp_name,
                                               rp_id=rp_id)

    def get_webauthn_assertion_options(self, user_id, *, challenge, rp_id):
        """
        Returns a dictionary of assertion options suitable for beginning the WebAuthn
        authentication process for the given user.
        """
        user = self.get_user(user_id)

        return webauthn.get_assertion_options(user,
                                              challenge=challenge,
                                              rp_id=rp_id)

    def verify_webauthn_credential(self, credential, *, challenge, rp_id,
                                   origin):
        """
        Checks whether the given credential is valid, i.e. suitable for generating
        assertions during authentication.

        Returns the validated credential on success, raises
        webauthn.RegistrationRejectedException on failure.
        """
        validated_credential = webauthn.verify_registration_response(
            credential, challenge=challenge, rp_id=rp_id, origin=origin)

        webauthn_cred = (self.db.query(WebAuthn).filter_by(
            credential_id=validated_credential.credential_id.decode()).first())

        if webauthn_cred is not None:
            raise webauthn.RegistrationRejectedException(
                "Credential ID already in use")

        return validated_credential

    def verify_webauthn_assertion(self, user_id, assertion, *, challenge,
                                  origin, rp_id):
        """
        Checks whether the given assertion was produced by the given user's WebAuthn
        device.

        Returns the updated signage count on success, raises
        webauthn.AuthenticationRejectedException on failure.
        """
        user = self.get_user(user_id)

        return webauthn.verify_assertion_response(assertion,
                                                  challenge=challenge,
                                                  user=user,
                                                  origin=origin,
                                                  rp_id=rp_id)

    def add_webauthn(self, user_id, **kwargs):
        """
        Adds a WebAuthn credential to the given user.

        Returns None if the user already has this credential.
        """
        user = self.get_user(user_id)

        webauthn = WebAuthn(user=user, **kwargs)
        self.db.add(webauthn)
        self.db.flush()  # flush the db now so webauthn.id is available

        return webauthn

    def get_webauthn_by_label(self, user_id, label):
        """
        Returns a WebAuthn credential for the given user by its label,
        or None if no credential for the user has this label.
        """
        user = self.get_user(user_id)

        return next(
            (credential
             for credential in user.webauthn if credential.label == label),
            None,
        )

    def get_webauthn_by_credential_id(self, user_id, credential_id):
        """
        Returns a WebAuthn credential for the given user by its credential ID,
        or None of the user doesn't have a credential with this ID.
        """
        user = self.get_user(user_id)

        return next(
            (credential for credential in user.webauthn
             if credential.credential_id == credential_id),
            None,
        )
Exemplo n.º 16
0
class DatabaseUserService:
    def __init__(self, session, *, ratelimiters=None, metrics):
        if ratelimiters is None:
            ratelimiters = {}
        ratelimiters = collections.defaultdict(DummyRateLimiter, ratelimiters)

        self.db = session
        self.ratelimiters = ratelimiters
        self.hasher = CryptContext(
            schemes=[
                "argon2",
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
            truncate_error=True,
            # Argon 2 Configuration
            argon2__memory_cost=1024,
            argon2__parallelism=6,
            argon2__time_cost=6,
        )
        self._metrics = metrics

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def get_user_by_username(self, username):
        user_id = self.find_userid(username)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def get_user_by_email(self, email):
        user_id = self.find_userid_by_email(email)
        return None if user_id is None else self.get_user(user_id)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = self.db.query(
                User.id).filter(User.username == username).one()
        except NoResultFound:
            return

        return user.id

    @functools.lru_cache()
    def find_userid_by_email(self, email):
        try:
            # flake8: noqa
            user_id = (self.db.query(
                Email.user_id).filter(Email.email == email).one())[0]
        except NoResultFound:
            return

        return user_id

    def check_password(self, userid, password, *, tags=None):
        tags = tags if tags is not None else []

        self._metrics.increment("warehouse.authentication.start", tags=tags)

        # The very first thing we want to do is check to see if we've hit our
        # global rate limit or not, assuming that we've been configured with a
        # global rate limiter anyways.
        if not self.ratelimiters["global"].test():
            logger.warning("Global failed login threshold reached.")
            self._metrics.increment(
                "warehouse.authentication.ratelimited",
                tags=tags + ["ratelimiter:global"],
            )
            raise TooManyFailedLogins(
                resets_in=self.ratelimiters["global"].resets_in())

        user = self.get_user(userid)
        if user is not None:
            # Now, check to make sure that we haven't hitten a rate limit on a
            # per user basis.
            if not self.ratelimiters["user"].test(user.id):
                self._metrics.increment(
                    "warehouse.authentication.ratelimited",
                    tags=tags + ["ratelimiter:user"],
                )
                raise TooManyFailedLogins(
                    resets_in=self.ratelimiters["user"].resets_in(user.id))

            # Actually check our hash, optionally getting a new hash for it if
            # we should upgrade our saved hashed.
            ok, new_hash = self.hasher.verify_and_update(
                password, user.password)

            # First, check to see if the password that we were given was OK.
            if ok:
                # Then, if the password was OK check to see if we've been given
                # a new password hash from the hasher, if so we'll want to save
                # that hash.
                if new_hash:
                    user.password = new_hash

                self._metrics.increment("warehouse.authentication.ok",
                                        tags=tags)

                return True
            else:
                self._metrics.increment(
                    "warehouse.authentication.failure",
                    tags=tags + ["failure_reason:password"],
                )
        else:
            self._metrics.increment("warehouse.authentication.failure",
                                    tags=tags + ["failure_reason:user"])

        # If we've gotten here, then we'll want to record a failed login in our
        # rate limiting before returning False to indicate a failed password
        # verification.
        if user is not None:
            self.ratelimiters["user"].hit(user.id)
        self.ratelimiters["global"].hit()

        return False

    def create_user(self, username, name, password):
        user = User(username=username,
                    name=name,
                    password=self.hasher.hash(password))
        self.db.add(user)
        self.db.flush()  # flush the db now so user.id is available

        return user

    def add_email(self, user_id, email_address, primary=None, verified=False):
        user = self.get_user(user_id)

        # If primary is None, then we're going to auto detect whether this should be the
        # primary address or not. The basic rule is that if the user doesn't already
        # have a primary address, then the address we're adding now is going to be
        # set to their primary.
        if primary is None:
            primary = True if user.primary_email is None else False

        email = Email(email=email_address,
                      user=user,
                      primary=primary,
                      verified=verified)
        self.db.add(email)
        self.db.flush()  # flush the db now so email.id is available

        return email

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            if attr == PASSWORD_FIELD:
                value = self.hasher.hash(value)
            setattr(user, attr, value)

        # If we've given the user a new password, then we also want to unset the
        # reason for disable... because a new password means no more disabled
        # user.
        if PASSWORD_FIELD in changes:
            user.disabled_for = None

        return user

    def disable_password(self, user_id, reason=None):
        user = self.get_user(user_id)
        user.password = self.hasher.disable()
        user.disabled_for = reason

    def is_disabled(self, user_id):
        user = self.get_user(user_id)

        # User is not disabled.
        if self.hasher.is_enabled(user.password):
            return (False, None)
        # User is disabled.
        else:
            return (True, user.disabled_for)
Exemplo n.º 17
0
class DatabaseUserService:
    def __init__(self, session, ratelimiters=None):
        if ratelimiters is None:
            ratelimiters = {}
        ratelimiters = collections.defaultdict(DummyRateLimiter, ratelimiters)

        self.db = session
        self.ratelimiters = ratelimiters
        self.hasher = CryptContext(
            schemes=[
                "argon2",
                "bcrypt_sha256",
                "bcrypt",
                "django_bcrypt",
                "unix_disabled",
            ],
            deprecated=["auto"],
            truncate_error=True,

            # Argon 2 Configuration
            argon2__memory_cost=1024,
            argon2__parallelism=6,
            argon2__time_cost=6,
        )

    @functools.lru_cache()
    def get_user(self, userid):
        # TODO: We probably don't actually want to just return the database
        #       object here.
        # TODO: We need some sort of Anonymous User.
        return self.db.query(User).get(userid)

    @functools.lru_cache()
    def find_userid(self, username):
        try:
            user = (self.db.query(
                User.id).filter(User.username == username).one())
        except NoResultFound:
            return

        return user.id

    @functools.lru_cache()
    def find_userid_by_email(self, email):
        try:
            # flake8: noqa
            user_id = (self.db.query(
                Email.user_id).filter(Email.email == email).one())[0]
        except NoResultFound:
            return

        return user_id

    def check_password(self, userid, password):
        # The very first thing we want to do is check to see if we've hit our
        # global rate limit or not, assuming that we've been configured with a
        # global rate limiter anyways.
        if not self.ratelimiters["global"].test():
            logger.warning("Global failed login threshold reached.")
            raise TooManyFailedLogins(
                resets_in=self.ratelimiters["global"].resets_in(), )

        user = self.get_user(userid)
        if user is not None:
            # Now, check to make sure that we haven't hitten a rate limit on a
            # per user basis.
            if not self.ratelimiters["user"].test(user.id):
                raise TooManyFailedLogins(
                    resets_in=self.ratelimiters["user"].resets_in(user.id), )

            # Actually check our hash, optionally getting a new hash for it if
            # we should upgrade our saved hashed.
            ok, new_hash = self.hasher.verify_and_update(
                password,
                user.password,
            )

            # First, check to see if the password that we were given was OK.
            if ok:
                # Then, if the password was OK check to see if we've been given
                # a new password hash from the hasher, if so we'll want to save
                # that hash.
                if new_hash:
                    user.password = new_hash

                return True

        # If we've gotten here, then we'll want to record a failed login in our
        # rate limiting before returning False to indicate a failed password
        # verification.
        if user is not None:
            self.ratelimiters["user"].hit(user.id)
        self.ratelimiters["global"].hit()

        return False

    def create_user(self,
                    username,
                    name,
                    password,
                    email,
                    is_active=False,
                    is_staff=False,
                    is_superuser=False):

        user = User(username=username,
                    name=name,
                    password=self.hasher.hash(password),
                    is_active=is_active,
                    is_staff=is_staff,
                    is_superuser=is_superuser)
        self.db.add(user)
        email_object = Email(email=email,
                             user=user,
                             primary=True,
                             verified=False)
        self.db.add(email_object)
        # flush the db now so user.id is available
        self.db.flush()
        return user

    def update_user(self, user_id, **changes):
        user = self.get_user(user_id)
        for attr, value in changes.items():
            if attr == PASSWORD_FIELD:
                value = self.hasher.hash(value)
            setattr(user, attr, value)
        return user

    def verify_email(self, user_id, email_address):
        user = self.get_user(user_id)
        for email in user.emails:
            if email.email == email_address:
                email.verified = True
Exemplo n.º 18
0
from passlib.context import CryptContext

hashes = ["pbkdf2_sha256", "md5_crypt", "des_crypt"]
deprecated = ["md5_crypt", "des_crypt"]
ctx = CryptContext(schemes=hashes, deprecated=deprecated)

serialized = ctx.to_string()
new_ctx = CryptContext.from_string(serialized)

res = ctx.hash("good password")
print(ctx.verify_and_update("good password", res))