-
Notifications
You must be signed in to change notification settings - Fork 423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Account plugin] add cache to authentication method #1413
Conversation
@Stanley thank you for investigating this. Are you willing to take over and fix the tests (basically remove the cache key when changing the password or deleting the account) or should we do it? |
Sure, I'll do it :) |
Thanks, let me know if you need help or if you want to pair on it. I am really exited by the feature to land 👍 |
Done. All tests are passing :) Anything else? |
Can you add a line in the CHANGELOG and your name in the CONTRIBUTORS file? |
Done :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! I recently notice this behavior when trying to switch from basicauth to the accounts plugin and to be honest I've been a long time trying to think of a solution. What worked for my was increasing the batch limit and holding more records at the client side.
I'm not by far a security expert, but in this case, don't we face the same security issue as we did by not rotating a key when using the embedded basicauth (see #691), but this time only at the cache layer? I mean, if an attacker gain access the cache layer and the HMAC secret, than we can have a password leakage.
I'm not saying this shouldn't land and to be honest I think that's a quite unlikely scenario. I'd be probably ok with running this in production, but maybe it would be safer to allow configuring and disabling this on settings.
# Check if password is valid (it is a very expensive computation) | ||
if bcrypt.checkpw(pwd_str, hashed): # Match! | ||
# Remember result so we don't have to calculate bcrypt hash next time. | ||
cache.set(cache_key, "1", ttl=30) # Time to live: 30 seconds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be great to expose the TTL in settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you think of a use case for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you think of a use case for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One can increase this value to have more cache hits (e.g across periodic requests). Refreshing the TTL at each request would also be a great feature IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought about it too, but couldn't figure out how to implement cache that would expire after one request. Any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry @gabisurita I missunderstood your comment yeah definitely 👍
No because it is a hmac and it is not reversible, however we can rotate the hmac key with this plugin since we can wipe the cache whenever we want. |
@Natim Sorry, I wasn't clear at this point. We're not leaking raw passwords but since we have a constant key/salt, one can brute force it with a dictionary attack over the hash. The problem isn't about being reversible, but being deterministic. I agree having a rotating secret and wiping out the cache periodically is a nice way to go, but it stills doesn't fix this problem, as an attacker can have access to both the hash and the seed at the same time. |
Oh right, we should use the username hash as the cache key and then have a random salt and a saltedhashed password as the value. Thanks for detecting this! |
Probably this hasn't survived the end of year crazyness :-) Here's a little bump -- what's missing on this to go ahead? |
I guess we should merge it !! |
We can merge once the following is done:
@Stanley are you still interested in working on this and go through those last steps? Let us know, maybe somebody can help and take over! Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for bringing this back to life!
There are a few things to fix though :)
settings = request.registry.settings | ||
hmac_secret = settings['userid_hmac_secret'] | ||
cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_CACHE_KEY.format(username)) | ||
cache_ttl = settings.get('account_cache_ttl_seconds', 30) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This setting should be documented and could be added to settings template IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you mean that https://github.com/Kinto/kinto/pull/1413/files#diff-2193261fa1533ff793bf90b447434bc8R32 is not sufficient?
cache = request.registry.cache | ||
cache_result = cache.get(cache_key) | ||
|
||
# Username and password is correct. No need to compare hashes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think is correct
applies here. It would probably be was already verified
|
||
# Username and password is correct. No need to compare hashes | ||
if cache_result: | ||
cache.expire(cache_key, cache_ttl) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment to say we refresh the cache ttl on each request?
kinto/plugins/accounts/views.py
Outdated
cache = request.registry.cache | ||
settings = request.registry.settings | ||
# Extract username and password from current user | ||
username, password = extract_http_basic_credentials(request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it be request.matchdict['id']
instead ? Since we make sure they match in the resource code?
tests/plugins/test_accounts.py
Outdated
|
||
mocked_bcrypt.checkpw.assert_called_once() | ||
|
||
def test_authentication_check_bcrypt_again_if_password_changes(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: checks
tests/plugins/test_accounts.py
Outdated
resp = self.app.get('/', headers=get_user_headers('me', 'bouh')) | ||
assert resp.json['user']['id'] == 'account:me' | ||
|
||
time.sleep(2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not slow the tests by two seconds here, we can do better like call .cache.expire(cache_key, 1)
first and then make sure it's >1
or whatever
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand how setting cache.expire helps here can you elaborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok got it
CHANGELOG.rst
Outdated
@@ -9,6 +9,7 @@ This document describes changes between each past release. | |||
**New features** | |||
|
|||
- Add Openid connect support (#939, #1425). See `demo <https://github.com/leplatrem/kinto-oidc-demo>`_ | |||
- Account plugin now caches authentication method (#1413) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Method? Hmm, I would say verification
docs/api/1.x/accounts.rst
Outdated
@@ -28,6 +28,9 @@ Add the following settings to the ``.ini`` file: | |||
# Allow anyone to create accounts. | |||
kinto.account_create_principals = system.Everyone | |||
|
|||
# Set the session time to live in seconds for the session |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: session time
and for the session
does not sound super clear in the same sentence :)
Thanks a lot @Stanley for starting this !!! |
When debugging my application, I've noticed very poor performance when logged in using account plugin. It turns out that the biggest bottle neck is bcrypt and its
hashpw
method (https://github.com/Kinto/kinto/blob/master/kinto/plugins/accounts/authentication.py#L18). Making sure it is executed no more than once per request drastically reduces response times.Before:
After:
kinto.tpl
file with it.