Repository: privacyidea/privacyidea

Issue:
is_challenge_response should not depent on the presence of a transaction_id in the database
The method ``is_challenge_response`` determines if the request is the response to a given challenge.
In this method it is also checked, if a challenge is available in the database table.
In certain scenarios this can lead to the effect, that privacyIDEA thinks, this request would not be a challenge.

We should remove the dependency on the availability of the challenge.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/token.py b/privacyidea/lib/token.py
--- a/privacyidea/lib/token.py
+++ b/privacyidea/lib/token.py
@@ -2138,8 +2138,13 @@ def check_token_list(tokenobject_list, passw, user=None, options=None, allow_res
                       tokenobject.user, tokenobject.get_serial()))
 
         if tokenobject.is_challenge_response(passw, user=user, options=options):
-            # This is a challenge response
-            challenge_response_token_list.append(tokenobject)
+            # This is a challenge response and it still has a challenge DB entry
+            if tokenobject.has_db_challenge_response(passw, user=user, options=options):
+                challenge_response_token_list.append(tokenobject)
+            else:
+                # This is a transaction_id, that either never existed or has expired.
+                # We add this to the invalid_token_list
+                invalid_token_list.append(tokenobject)
         elif tokenobject.is_challenge_request(passw, user=user,
                                               options=options):
             # This is a challenge request
diff --git a/privacyidea/lib/tokenclass.py b/privacyidea/lib/tokenclass.py
--- a/privacyidea/lib/tokenclass.py
+++ b/privacyidea/lib/tokenclass.py
@@ -453,7 +453,6 @@ def authenticate(self, passw, user=None, options=None):
 
         return pin_match, otp_counter, reply
 
-
     @staticmethod
     def decode_otpkey(otpkey, otpkeyformat):
         """
@@ -1387,8 +1386,8 @@ def is_challenge_request(self, passw, user=None, options=None):
 
     def is_challenge_response(self, passw, user=None, options=None):
         """
-        This method checks, if this is a request, that is the response to
-        a previously sent challenge.
+        This method checks, if this is a request that is supposed to be
+        the answer to a previous challenge.
 
         The default behaviour to check if this is the response to a
         previous challenge is simply by checking if the request contains
@@ -1397,6 +1396,7 @@ def is_challenge_response(self, passw, user=None, options=None):
 
         This method does not try to verify the response itself!
         It only determines, if this is a response for a challenge or not.
+        If the challenge still exists, is checked in has_db_challenge_response.
         The response is verified in check_challenge_response.
 
         :param passw: password, which might be pin or pin+otp
@@ -1409,11 +1409,27 @@ def is_challenge_response(self, passw, user=None, options=None):
         :rtype: bool
         """
         options = options or {}
+        transaction_id = options.get("transaction_id") or options.get("state")
+        return bool(transaction_id)
+
+    def has_db_challenge_response(self, passw, user=None, options=None):
+        """
+        This method checks, if the given transaction_id is actually the response to
+        a real challenge. To do so, it verifies, if there is a DB entry for the
+        given serial number and transaction_id.
+        This is to avoid side effects by passing non-existent transaction_ids.
+
+        This method checks, if the token still has a challenge
+        :param passw:
+        :param user:
+        :param options:
+        :return:
+        """
+        options = options or {}
         challenge_response = False
         transaction_id = options.get("transaction_id") or options.get("state")
         if transaction_id:
-            # Now we also need to check, if the transaction_id is an entry to the
-            # serial number of this token
+            # Now we also need to check, if there is a corresponding DB entry
             chals = get_challenges(serial=self.token.serial, transaction_id=transaction_id)
             challenge_response = bool(chals)
 


Test patch:
diff --git a/tests/test_lib_tokenclass.py b/tests/test_lib_tokenclass.py
--- a/tests/test_lib_tokenclass.py
+++ b/tests/test_lib_tokenclass.py
@@ -563,11 +563,19 @@ def test_18_challenges(self):
                                                 realm=self.realm1),
                                             "test123456")
         self.assertFalse(resp, resp)
+
+        # A the token has not DB entry in the challenges table, basically
+        # "is_cahllenge_response"
         resp = token.is_challenge_response(User(login="cornelius",
                                                 realm=self.realm1),
                                             "test123456",
                                             options={"transaction_id": transaction_id})
-        # The token has not DB entry in the challenges table
+        self.assertTrue(resp)
+        # ... but is does not "has_db_challenge_response"
+        resp = token.has_db_challenge_response(User(login="cornelius",
+                                                realm=self.realm1),
+                                               "test123456",
+                                               options={"transaction_id": transaction_id})
         self.assertFalse(resp)
 
         # Create a challenge