Repository: privacyidea/privacyidea

Issue:
The TiQR Token should return the remaining attempts before the token is blocked
The INVALID_RESPONSE in the TiQR token returns the number of remaining PIN attempts in ` ``INVALIDE_RESPONSE:<n>``. Currently the ``:<n>`` is not implemented.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/tokens/ocra.py b/privacyidea/lib/tokens/ocra.py
--- a/privacyidea/lib/tokens/ocra.py
+++ b/privacyidea/lib/tokens/ocra.py
@@ -37,7 +37,6 @@
 import binascii
 import struct
 
-
 SHA_FUNC = {"SHA1": sha1,
             "SHA256": sha256,
             "SHA512": sha512}
@@ -184,7 +183,8 @@ def create_challenge(self):
         """
         ret = None
         if self.challenge_type == "QH":
-            ret = geturandom(length=self.challenge_length, hex=True)
+            ret = geturandom(length=int(round(self.challenge_length/2)), hex=True)
+            ret = ret[:self.challenge_length]
         elif self.challenge_type == "QA":
             ret = get_alphanum_str(self.challenge_length)
         elif self.challenge_type == "QN":
diff --git a/privacyidea/lib/tokens/tiqrtoken.py b/privacyidea/lib/tokens/tiqrtoken.py
--- a/privacyidea/lib/tokens/tiqrtoken.py
+++ b/privacyidea/lib/tokens/tiqrtoken.py
@@ -331,7 +331,6 @@ def api_endpoint(cls, request, g):
                     token = get_one_token(serial=challenge.serial)
                     if token.type.lower() == "tiqr":
                         # We found a TiQR token with a valid challenge with the given transaction ID
-                        res = "INVALID_RESPONSE"
                         r = token.verify_response(
                             challenge=challenge.challenge, passw=passw)
                         if r > 0:
@@ -340,6 +339,14 @@ def api_endpoint(cls, request, g):
                             challenge.set_otp_status(True)
                             # We have found a valid TiQR token transaction, we break out of the loop
                             break
+                        else:
+                            # Send back how may retries there are left for the token is blocked
+                            token.inc_failcount()
+                            fail = token.get_failcount()
+                            maxfail = token.get_max_failcount()
+                            res = "INVALID_RESPONSE:{0!s}".format(maxfail - fail)
+                            break
+
             cleanup_challenges()
 
             return "plain", res
@@ -378,6 +385,8 @@ def create_challenge(self, transactionid=None, options=None):
 
         service_identifier = get_from_config("tiqr.serviceIdentifier") or \
                              "org.privacyidea"
+        service_displayname = get_from_config("tiqr.serviceDisplayname") or \
+                              "privacyIDEA"
 
         # Get the OCRASUITE from the token information
         ocrasuite = self.get_tokeninfo("ocrasuite") or OCRA_DEFAULT_SUITE
@@ -396,11 +405,13 @@ def create_challenge(self, transactionid=None, options=None):
 
         # Encode the user to UTF-8 and quote the result
         encoded_user_identifier = quote_plus(user_identifier.encode('utf-8'))
-        authurl = u"tiqrauth://{0!s}@{1!s}/{2!s}/{3!s}".format(
+        authurl = u"tiqrauth://{0!s}@{1!s}/{2!s}/{3!s}/{4!s}".format(
                                               encoded_user_identifier,
                                               service_identifier,
                                               db_challenge.transaction_id,
-                                              challenge)
+                                              challenge,
+                                              service_displayname
+                                              )
         attributes = {"img": create_img(authurl, width=250),
                       "value": authurl,
                       "poll": True,


Test patch:
diff --git a/tests/test_lib_tokens_tiqr.py b/tests/test_lib_tokens_tiqr.py
--- a/tests/test_lib_tokens_tiqr.py
+++ b/tests/test_lib_tokens_tiqr.py
@@ -126,7 +126,7 @@ def test_02_create_challenge(self):
         # test creation of hex challenge
         os = OCRASuite("OCRA-1:HOTP-SHA1-6:QH10-S128")
         c = os.create_challenge()
-        self.assertEqual(len(c), 20)
+        self.assertEqual(len(c), 10)
         self.assertTrue("G" not in c, c)
 
         # test creation of alphanum challenge
@@ -529,7 +529,7 @@ def _test_api_endpoint(self, user, expected_netloc):
                         "operation": "login"}
         r = TiqrTokenClass.api_endpoint(req, g)
         self.assertEqual(r[0], "plain")
-        self.assertEqual(r[1], "INVALID_RESPONSE")
+        self.assertRegexpMatches(r[1], r"INVALID_RESPONSE:[0-9]+")
 
         # Check that the OTP status is still incorrect
         r = token.check_challenge_response(options={"transaction_id":
@@ -684,7 +684,7 @@ def test_05_api_endpoint_with_multiple_tokens(self):
                         "operation": "login"}
         r = TiqrTokenClass.api_endpoint(req, g)
         self.assertEqual(r[0], "plain")
-        self.assertEqual(r[1], "INVALID_RESPONSE")
+        self.assertRegexpMatches(r[1], r"INVALID_RESPONSE:[0-9]+")
 
         # Check that the OTP status is still incorrect
         r = token.check_challenge_response(options={"transaction_id":
@@ -719,4 +719,4 @@ def test_05_api_endpoint_with_multiple_tokens(self):
         # challenge was deleted from the database
         r = token.check_challenge_response(options={"transaction_id":
                                                     transaction_id})
-        self.assertTrue(r < 0, r)
\ No newline at end of file
+        self.assertTrue(r < 0, r)