Skip to content
This repository has been archived by the owner on Jul 30, 2024. It is now read-only.

in memory cache verify password implementation #839

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ Core
``SECURITY_DEFAULT_HTTP_AUTH_REALM`` Specifies the default authentication
realm when using basic HTTP auth.
Defaults to ``Login Required``
``USE_VERIFY_PASSWORD_CACHE`` If ``True`` Enables cache for token
verification, make it quicker further
calls to authenticated routes using
token and slow hash algorithms
(like bcrypt). Defaults to ``None``
``VERIFY_HASH_CACHE_MAX_SIZE`` Limitation for token validation cache size
Rules are the ones of TTLCache of
cachetools package. Defaults to
``500``
``VERIFY_HASH_CACHE_TTL`` Time to live for password check cache entries.
Defaults to ``300`` (5 minutes)
======================================== =======================================


Expand Down
26 changes: 26 additions & 0 deletions flask_security/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from flask import current_app
from cachetools import TTLCache


class VerifyHashCache:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to be 2.7 compatible this will need to be: VerifyHashCache(object):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not an issue for me.

image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The drawbacks of not doing so is that some class related feature are not available with a class declared this way and some of these points are described here https://stackoverflow.com/questions/4015417/python-class-inherits-object

The cache object I made does not uses any of these feature, so this is not required, but I guess for the sake of python the inheritance should be written.

On the other hand 2020 is comming very fast :)

"""Cache handler to make it quick password check by bypassing
already checked passwords against exact same couple of token/password.
This cache handler is more efficient on small apps that
run on few processes as cache is only shared between threads."""

def __init__(self):
ttl = current_app.config.get("VERIFY_HASH_CACHE_TTL", 60 * 5)
max_size = current_app.config.get("VERIFY_HASH_CACHE_MAX_SIZE", 500)
self._cache = TTLCache(max_size, ttl)

def has_verify_hash_cache(self, user):
"""Check given user id is in cache."""
return self._cache.get(user.id)

def set_cache(self, user):
"""When a password is checked, then result is put in cache."""
self._cache[user.id] = True

def clear(self):
"""Clear cache"""
self._cache.clear()
27 changes: 23 additions & 4 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from itsdangerous import URLSafeTimedSerializer
from passlib.context import CryptContext
from werkzeug.datastructures import ImmutableList
from werkzeug.local import LocalProxy
from werkzeug.local import LocalProxy, Local

from .forms import ChangePasswordForm, ConfirmRegisterForm, \
ForgotPasswordForm, LoginForm, PasswordlessLoginForm, RegisterForm, \
Expand All @@ -34,9 +34,11 @@
from .utils import get_config, hash_data, localize_callback, send_mail, \
string_types, url_for_security, verify_and_update_password, verify_hash
from .views import create_blueprint
from .cache import VerifyHashCache

# Convenient references
_security = LocalProxy(lambda: current_app.extensions['security'])
local_cache = Local()


#: Default Flask-Security configuration
Expand Down Expand Up @@ -245,14 +247,31 @@ def _request_loader(request):
if isinstance(data, dict):
token = data.get(args_key, token)

use_cache = current_app.config.get('USE_VERIFY_PASSWORD_CACHE')

try:
data = _security.remember_token_serializer.loads(
token, max_age=_security.token_max_age)
user = _security.datastore.find_user(id=data[0])
if user and verify_hash(data[1], user.password):
return user
except:
pass
user = None

if not user:
return _security.login_manager.anonymous_user()
if use_cache:
cache = getattr(local_cache, 'verify_hash_cache', None)
if cache is None:
cache = VerifyHashCache()
local_cache.verify_hash_cache = cache
if cache.has_verify_hash_cache(user):
return user
if verify_hash(data[1], user.password):
cache.set_cache(user)
return user
else:
if verify_hash(data[1], user.password):
return user

return _security.login_manager.anonymous_user()


Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
'Flask-BabelEx>=0.9.3',
'itsdangerous>=0.21',
'passlib>=1.7',
'cachetools>=3.1.0',
]

packages = find_packages()
Expand Down
124 changes: 124 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
"""
test_cache
~~~~~~~~~~~~

verify hash cache tests
"""

from flask_security.cache import VerifyHashCache
from flask_security.core import _request_loader, local_cache


class MockRequest:
@property
def headers(self):
return {"token-header": "mock-token-header"}

@property
def args(self):
return {}

@property
def is_json(self):
return True

def get_json(self, silent=None):
return {}


class MockUser:
def __init__(self, id, password):
self.id = id
self.password = password


class MockExtensionSecurity:
@property
def token_authentication_header(self):
return "token-header"

@property
def token_authentication_key(self):
return "token-key"

@property
def login_manager(self):
class MockLoginManager:
def anonymous_user(self):
return None

return MockLoginManager()

@property
def remember_token_serializer(self):
class MockLoader:
def loads(self, token, max_age):
return [1, "token"]

return MockLoader()

@property
def token_max_age(self):
return 1

@property
def datastore(self):
class MockDataStore:
def find_user(self, id=None):
return MockUser(id, "token")

return MockDataStore()

@property
def hashing_context(self):
class MockHashingContext:
def verify(self, encoded_data, hashed_data):
return encoded_data.decode() == hashed_data

return MockHashingContext()


def test_verify_password_cache_init(app):
with app.app_context():
vhc = VerifyHashCache()
assert len(vhc._cache) == 0
assert vhc._cache.ttl == 60 * 5
assert vhc._cache.maxsize == 500
app.config["VERIFY_HASH_CACHE_TTL"] = 10
app.config["VERIFY_HASH_CACHE_MAX_SIZE"] = 10
vhc = VerifyHashCache()
assert vhc._cache.ttl == 10
assert vhc._cache.maxsize == 10


def test_verify_password_cache_set_get(app):
class MockUser:
def __init__(self, id):
self.id = id

user = MockUser(1)
with app.app_context():
vhc = VerifyHashCache()
assert vhc.has_verify_hash_cache(user) is None
vhc.set_cache(user)
assert len(vhc._cache) == 1
assert vhc.has_verify_hash_cache(user)
vhc.clear()
assert vhc.has_verify_hash_cache(user) is None


def test_request_loader_not_using_cache(app):
with app.app_context():
app.extensions["security"] = MockExtensionSecurity()
_request_loader(MockRequest())
assert getattr(local_cache, "verify_hash_cache", None) is None


def test_request_loader_using_cache(app):
with app.app_context():
app.config["USE_VERIFY_PASSWORD_CACHE"] = True
app.extensions["security"] = MockExtensionSecurity()
_request_loader(MockRequest())
assert local_cache.verify_hash_cache is not None
assert local_cache.verify_hash_cache.has_verify_hash_cache(MockUser(1, "token"))