Repository: privacyidea/privacyidea

Issue:
Auth Cache policy does not add entries to the authcache table
Currently the auth cache policy does not add entries to the auth cache, it just tries to validate given authentications.



Discussion:

Resolving patch:
diff --git a/privacyidea/lib/authcache.py b/privacyidea/lib/authcache.py
--- a/privacyidea/lib/authcache.py
+++ b/privacyidea/lib/authcache.py
@@ -19,21 +19,26 @@
 # You should have received a copy of the GNU Affero General Public
 # License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
+import binascii
 
 from ..models import AuthCache, db
 from sqlalchemy import and_
-from hashlib import sha256
-from binascii import hexlify
+from privacyidea.lib.crypto import hash
 import datetime
 import logging
 
 log = logging.getLogger(__name__)
 
 
-def add_to_cache(username, realm, resolver, auth_hash):
+def _hash_password(password):
+    return binascii.hexlify(hash(password, seed=""))
+
+
+def add_to_cache(username, realm, resolver, password):
     # Can not store timezone aware timestamps!
     first_auth = datetime.datetime.utcnow()
-    record = AuthCache(username, realm, resolver, auth_hash, first_auth)
+    auth_hash = _hash_password(password)
+    record = AuthCache(username, realm, resolver, auth_hash, first_auth, first_auth)
     log.debug('Adding record to auth cache: ({!r}, {!r}, {!r}, {!r})'.format(
         username, realm, resolver, auth_hash))
     r = record.save()
@@ -47,12 +52,12 @@ def update_cache_last_auth(cache_id):
     db.session.commit()
 
 
-def delete_from_cache(username, realm, resolver, auth_hash):
+def delete_from_cache(username, realm, resolver, password):
     r = db.session.query(AuthCache).filter(AuthCache.username == username,
                                        AuthCache.realm == realm,
                                        AuthCache.resolver == resolver,
                                        AuthCache.authentication ==
-                                       auth_hash).delete()
+                                       _hash_password(password)).delete()
     db.session.commit()
     return r
 
@@ -77,7 +82,7 @@ def verify_in_cache(username, realm, resolver, password,
     conditions.append(AuthCache.username == username)
     conditions.append(AuthCache.realm == realm)
     conditions.append(AuthCache.resolver == resolver)
-    auth_hash = hexlify(sha256(password).digest())
+    auth_hash = _hash_password(password)
     conditions.append(AuthCache.authentication == auth_hash)
 
     if first_auth:
@@ -95,7 +100,7 @@ def verify_in_cache(username, realm, resolver, password,
 
     else:
         # Delete older entries
-        delete_from_cache(username, realm, resolver, auth_hash)
+        delete_from_cache(username, realm, resolver, password)
 
     return result
 
diff --git a/privacyidea/lib/policydecorators.py b/privacyidea/lib/policydecorators.py
--- a/privacyidea/lib/policydecorators.py
+++ b/privacyidea/lib/policydecorators.py
@@ -47,7 +47,7 @@
 from privacyidea.lib.policy import ACTION, SCOPE, ACTIONVALUE, LOGINMODE
 from privacyidea.lib.user import User
 from privacyidea.lib.utils import parse_timelimit, parse_timedelta
-from privacyidea.lib.authcache import verify_in_cache
+from privacyidea.lib.authcache import verify_in_cache, add_to_cache
 import datetime
 from dateutil.tz import tzlocal
 from privacyidea.lib.radiusserver import get_radius
@@ -149,7 +149,7 @@ def challenge_response_wrapper(*args, **kwds):
 def auth_cache(wrapped_function, user_object, passw, options=None):
     """
     Decorate lib.token:check_user_pass. Verify, if the authentication can 
-    be found in the auth_cache. 
+    be found in the auth_cache.
     
     :param wrapped_function: usually "check_user_pass"
     :param user_object: User who tries to authenticate
@@ -159,6 +159,7 @@ def auth_cache(wrapped_function, user_object, passw, options=None):
     """
     options = options or {}
     g = options.get("g")
+    auth_cache = None
     if g:
         clientip = options.get("clientip")
         policy_object = g.policy_object
@@ -192,8 +193,12 @@ def auth_cache(wrapped_function, user_object, passw, options=None):
             if result:
                 return True, {"message": "Authenticated by AuthCache."}
 
-    # If nothing else returned, we return the wrapped function
-    return wrapped_function(user_object, passw, options)
+    # If nothing else returned, call the wrapped function
+    res, reply_dict = wrapped_function(user_object, passw, options)
+    if auth_cache and res:
+        # If authentication is successful, we store the password in auth_cache
+        add_to_cache(user_object.login, user_object.realm, user_object.resolver, passw)
+    return res, reply_dict
 
 
 def auth_user_has_no_token(wrapped_function, user_object, passw,


Test patch:
diff --git a/tests/test_api_validate.py b/tests/test_api_validate.py
--- a/tests/test_api_validate.py
+++ b/tests/test_api_validate.py
@@ -5,7 +5,8 @@
 from .base import MyTestCase
 from privacyidea.lib.user import (User)
 from privacyidea.lib.tokens.totptoken import HotpTokenClass
-from privacyidea.models import (Token, Challenge)
+from privacyidea.models import (Token, Challenge, AuthCache)
+from privacyidea.lib.authcache import _hash_password
 from privacyidea.lib.config import (set_privacyidea_config, get_token_types,
                                     get_inc_fail_count_on_false_pin,
                                     delete_privacyidea_config)
@@ -2250,6 +2251,69 @@ def test_31_count_auth(self):
         remove_token(serial)
         delete_privacyidea_config("no_auth_counter")
 
+    def test_33_auth_cache(self):
+        init_token({"otpkey": self.otpkey},
+                   user=User("cornelius", self.realm1))
+        set_policy(name="authcache", action="{0!s}=4m".format(ACTION.AUTH_CACHE), scope=SCOPE.AUTH)
+        with self.app.test_request_context('/validate/check',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "realm": self.realm1,
+                                                 "pass": OTPs[1]}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertTrue(result.get("status"))
+            self.assertTrue(result.get("value"))
+
+        # Check that there is an entry with this OTP value in the auth_cache
+        r = AuthCache.query.filter(AuthCache.authentication == _hash_password(OTPs[1])).first()
+        self.assertTrue(bool(r))
+
+        # Authenticate again with the same OTP
+        with self.app.test_request_context('/validate/check',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "realm": self.realm1,
+                                                 "pass": OTPs[1]}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertTrue(result.get("status"))
+            self.assertTrue(result.get("value"))
+            detail = json.loads(res.data.decode('utf8')).get("detail")
+            self.assertEqual(detail.get("message"), u"Authenticated by AuthCache.")
+
+        delete_policy("authcache")
+
+        # If there is no authcache, the same value must not be used again!
+        with self.app.test_request_context('/validate/check',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "realm": self.realm1,
+                                                 "pass": OTPs[2]}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertTrue(result.get("status"))
+            self.assertTrue(result.get("value"))
+
+        # Check that there is no entry with this OTP value in the auth_cache
+        r = AuthCache.query.filter(AuthCache.authentication == _hash_password(OTPs[2])).first()
+        self.assertFalse(bool(r))
+
+        # Authenticate again with the same OTP value will fail
+        with self.app.test_request_context('/validate/check',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "realm": self.realm1,
+                                                 "pass": OTPs[2]}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertTrue(result.get("status"))
+            self.assertFalse(result.get("value"))
+
 
 class AChallengeResponse(MyTestCase):
 
diff --git a/tests/test_lib_authcache.py b/tests/test_lib_authcache.py
--- a/tests/test_lib_authcache.py
+++ b/tests/test_lib_authcache.py
@@ -6,11 +6,10 @@
 from .base import MyTestCase
 
 from privacyidea.lib.authcache import (add_to_cache, delete_from_cache,
-                                       update_cache_last_auth, verify_in_cache)
+                                       update_cache_last_auth, verify_in_cache,
+                                       _hash_password)
 from privacyidea.models import AuthCache
 import datetime
-import hashlib
-import binascii
 
 
 class AuthCacheTestCase(MyTestCase):
@@ -21,29 +20,27 @@ class AuthCacheTestCase(MyTestCase):
     username = "hans"
     realm = "realm"
     resolver = "resolver"
-    pw_hash = binascii.hexlify(hashlib.sha256(password).digest())
 
     def test_01_write_update_delete_cache(self):
         teststart = datetime.datetime.utcnow()
 
-        r = add_to_cache(self.username, self.realm, self.resolver,
-                         auth_hash=self.pw_hash)
+        r = add_to_cache(self.username, self.realm, self.resolver, self.password)
 
         self.assertTrue(r > 0)
 
         auth = AuthCache.query.filter(AuthCache.id == r).first()
         self.assertEqual(auth.username, self.username)
-        self.assertEqual(auth.authentication, self.pw_hash)
+        self.assertEqual(auth.authentication, _hash_password(self.password))
 
         self.assertTrue(auth.first_auth > teststart)
-        self.assertEqual(auth.last_auth, None)
+        self.assertEqual(auth.last_auth, auth.first_auth)
 
         update_cache_last_auth(r)
         auth = AuthCache.query.filter(AuthCache.id == r).first()
         self.assertTrue(auth.last_auth > teststart)
 
         r_delete = delete_from_cache(self.username, self.realm, self.resolver,
-                                     self.pw_hash)
+                                     self.password)
         self.assertEqual(r, r_delete)
 
         auth = AuthCache.query.filter(AuthCache.username ==
@@ -57,8 +54,7 @@ def test_02_verify_cache(self):
         self.assertFalse(r)
 
         # Add Entry to cache
-        r = add_to_cache(self.username, self.realm, self.resolver,
-                     auth_hash=self.pw_hash)
+        r = add_to_cache(self.username, self.realm, self.resolver, self.password)
         update_cache_last_auth(r)
         auth = AuthCache.query.filter(AuthCache.id == r).first()
         last_auth1 = auth.last_auth
@@ -78,7 +74,7 @@ def test_02_verify_cache(self):
 
     def test_03_delete_old_entries(self):
         # Create a VERY old authcache entry
-        AuthCache("grandpa", self.realm, self.resolver, self.pw_hash,
+        AuthCache("grandpa", self.realm, self.resolver, _hash_password(self.password),
                   first_auth=datetime.datetime.utcnow() - datetime.timedelta(
                       days=10),
                   last_auth=datetime.datetime.utcnow() - datetime.timedelta(
diff --git a/tests/test_lib_policydecorator.py b/tests/test_lib_policydecorator.py
--- a/tests/test_lib_policydecorator.py
+++ b/tests/test_lib_policydecorator.py
@@ -32,7 +32,7 @@
 import binascii
 import hashlib
 from privacyidea.models import AuthCache
-from privacyidea.lib.authcache import delete_from_cache
+from privacyidea.lib.authcache import delete_from_cache, _hash_password
 from datetime import timedelta
 
 
@@ -523,7 +523,6 @@ def test_12_authcache(self):
         username = "cornelius"
         realm = "myrealm"
         resolver = "reso001"
-        pw_hash = binascii.hexlify(hashlib.sha256(password).digest())
 
         r = save_resolver({"resolver": "reso001",
                            "type": "passwdresolver",
@@ -545,7 +544,7 @@ def fake_check_user_pass(user, passw, options=None):
 
         # This successfully authenticates against the authcache
         # We have an authentication, that is within the policy timeout
-        AuthCache(username, realm, resolver, pw_hash,
+        AuthCache(username, realm, resolver, _hash_password(password),
                   first_auth=datetime.datetime.utcnow() - timedelta(hours=3),
                   last_auth=datetime.datetime.utcnow() - timedelta(minutes=1)).save()
         r = auth_cache(fake_check_user_pass, User("cornelius", "myrealm"),
@@ -555,8 +554,8 @@ def fake_check_user_pass(user, passw, options=None):
 
         # We have an authentication, that is not read from the authcache,
         # since the authcache first_auth is too old.
-        delete_from_cache(username, realm, resolver, pw_hash)
-        AuthCache(username, realm, resolver, pw_hash,
+        delete_from_cache(username, realm, resolver, password)
+        AuthCache(username, realm, resolver, _hash_password(password),
                   first_auth=datetime.datetime.utcnow() - timedelta(hours=5),
                   last_auth=datetime.datetime.utcnow() - timedelta(
                       minutes=1)).save()
@@ -567,8 +566,8 @@ def fake_check_user_pass(user, passw, options=None):
 
         # We have an authentication, that is not read from authcache, since
         # the last_auth is too old = 10 minutes.
-        delete_from_cache(username, realm, resolver, pw_hash)
-        AuthCache(username, realm, resolver, pw_hash,
+        delete_from_cache(username, realm, resolver, password)
+        AuthCache(username, realm, resolver, _hash_password(password),
                   first_auth=datetime.datetime.utcnow() - timedelta(hours=1),
                   last_auth=datetime.datetime.utcnow() - timedelta(
                       minutes=10)).save()
@@ -589,8 +588,8 @@ def fake_check_user_pass(user, passw, options=None):
         g.policy_object = P
         options = {"g": g}
 
-        delete_from_cache(username, realm, resolver, pw_hash)
-        AuthCache(username, realm, resolver, pw_hash,
+        delete_from_cache(username, realm, resolver, password)
+        AuthCache(username, realm, resolver, _hash_password(password),
                   first_auth=datetime.datetime.utcnow() - timedelta(hours=2),
                   last_auth=datetime.datetime.utcnow() - timedelta(
                       hours=1)).save()