Repository: privacyidea/privacyidea

Issue:
RegistrationToken can not do Challenge Response.
The Registration token can not be used as a Challenge Response token.
This is due to the fact, that the token inherits the ``is_challenge_request`` from the original ``Tokenclass``. There is is required that "data" or "challenge" is contained in the options dict. This is not available.

The baseclass ``PasswordToken`` should have the same ``is_challenge_request``  like the HOTPTokenClass - and simply ignore the need for "data" or "challenge".


Discussion:
We can check, if the "data" and "challenge" check is actually necessary in the base class and if we simply remove this from the baseclass.
Then we could also remove the ``is_challenge_request`` from the HOTPTokenClass.
Currently these tokens do not have their own mehtod ``is_challenge_request``

- certificatetoken.py 
- daplugtoken.py  inherited from HOTP
- foureyestoken.py 
- motptoken.py
- papertoken.py  inherited from HOTP
- passwordtoken.py
- registrationtoken.py  inherited from Password
- sshkeytoken.py
- tantoken.py  inherited from Paper
- tiqrtoken.py  inherited from OCRA
- totptoken.py inherited from HOTP
- vascotoken.py
- yubicotoken.py

We would probable expect from motp, password, vasco and yubico to be potentially used in challenge response mode. 
We can not expect this from certificate, sshkey or 4eyes in the default way.

None of the tokens with their own ``is_challenge_request`` method are calling the method from the superclass, so we can safely safe the method in the baseclass and move the method from the HOTP token to the baseclass.


Resolving patch:
diff --git a/privacyidea/lib/tokenclass.py b/privacyidea/lib/tokenclass.py
--- a/privacyidea/lib/tokenclass.py
+++ b/privacyidea/lib/tokenclass.py
@@ -1354,11 +1354,10 @@ def is_challenge_request(self, passw, user=None, options=None):
         :return: true or false
         :rtype: bool
         """
-
         request_is_challenge = False
         options = options or {}
         pin_match = self.check_pin(passw, user=user, options=options)
-        if pin_match is True and "data" in options or "challenge" in options:
+        if pin_match is True:
             request_is_challenge = True
 
         return request_is_challenge
diff --git a/privacyidea/lib/tokens/hotptoken.py b/privacyidea/lib/tokens/hotptoken.py
--- a/privacyidea/lib/tokens/hotptoken.py
+++ b/privacyidea/lib/tokens/hotptoken.py
@@ -350,31 +350,6 @@ def hashlib(self):
                      get_from_config("hotp.hashlib", u'sha1')
         return hashlibStr
 
-    # challenge interfaces starts here
-    @log_with(log)
-    @challenge_response_allowed
-    def is_challenge_request(self, passw, user=None, options=None):
-        """
-        check, if the request would start a challenge
-
-        - default: if the passw contains only the pin, this request would
-        trigger a challenge
-
-        - in this place as well the policy for a token is checked
-
-        :param passw: password, which might be pin or pin+otp
-        :param options: dictionary of additional request parameters
-
-        :return: returns true or false
-        """
-        trigger_challenge = False
-        options = options or {}
-        pin_match = self.check_pin(passw, user=user, options=options)
-        if pin_match is True:
-            trigger_challenge = True
-
-        return trigger_challenge
-
 
     @log_with(log)
     @check_token_locked


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
@@ -4,6 +4,7 @@
 from .base import MyApiTestCase
 from privacyidea.lib.user import (User)
 from privacyidea.lib.tokens.totptoken import HotpTokenClass
+from privacyidea.lib.tokens.registrationtoken import RegistrationTokenClass
 from privacyidea.models import (Token, Challenge, AuthCache, db)
 from privacyidea.lib.authcache import _hash_password
 from privacyidea.lib.config import (set_privacyidea_config, get_token_types,
@@ -1003,6 +1004,31 @@ def test_10_saml_check(self):
             self.assertEqual(attributes.get("username"), "cornelius")
 
     def test_11_challenge_response_hotp(self):
+        serial = "CHALRESP1"
+        pin = "chalresp1"
+        # create a token and assign to the user
+        db_token = Token(serial, tokentype="hotp")
+        db_token.update_otpkey(self.otpkey)
+        db_token.save()
+        token = HotpTokenClass(db_token)
+        token.add_user(User("cornelius", self.realm1))
+        token.set_pin(pin)
+        # Set the failcounter
+        token.set_failcount(5)
+
+        # try to do challenge response without a policy. It will fail
+        with self.app.test_request_context('/validate/check',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "pass": pin}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            detail = res.json.get("detail")
+            self.assertFalse(result.get("value"))
+            self.assertEqual(detail.get("message"), _("wrong otp pin"))
+            self.assertNotIn("transaction_id", detail)
+
         # set a chalresp policy for HOTP
         with self.app.test_request_context('/policy/pol_chal_resp',
                                            data={'action':
@@ -1018,17 +1044,6 @@ def test_11_challenge_response_hotp(self):
             self.assertTrue(result["status"] is True, result)
             self.assertEquals(result['value']['setPolicy pol_chal_resp'], 1, result)
 
-        serial = "CHALRESP1"
-        pin = "chalresp1"
-        # create a token and assign to the user
-        db_token = Token(serial, tokentype="hotp")
-        db_token.update_otpkey(self.otpkey)
-        db_token.save()
-        token = HotpTokenClass(db_token)
-        token.add_user(User("cornelius", self.realm1))
-        token.set_pin(pin)
-        # Set the failcounter
-        token.set_failcount(5)
         # create the challenge by authenticating with the OTP PIN
         with self.app.test_request_context('/validate/check',
                                            method='POST',
@@ -1060,6 +1075,61 @@ def test_11_challenge_response_hotp(self):
         # delete the token
         remove_token(serial=serial)
 
+    def test_11a_challenge_response_registration(self):
+        serial = "CHALRESP1"
+        pin = "chalresp1"
+        # create a token and assign to the user
+        db_token = Token(serial, tokentype="registration")
+        db_token.save()
+        token = RegistrationTokenClass(db_token)
+        token.add_user(User("cornelius", self.realm1))
+        token.set_pin(pin)
+
+        # try to do challenge response without a policy. It will fail
+        with self.app.test_request_context('/validate/check',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "pass": pin}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            detail = res.json.get("detail")
+            self.assertFalse(result.get("value"))
+            self.assertEqual(detail.get("message"), _("wrong otp pin"))
+            self.assertNotIn("transaction_id", detail)
+
+        # set a chalresp policy for Registration Token
+        with self.app.test_request_context('/policy/pol_chal_resp',
+                                           data={'action':
+                                                     "challenge_response=registration",
+                                                 'scope': "authentication",
+                                                 'realm': '',
+                                                 'active': True},
+                                           method='POST',
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            self.assertTrue(result["status"] is True, result)
+            self.assertEquals(result['value']['setPolicy pol_chal_resp'], 1, result)
+
+        # create the challenge by authenticating with the OTP PIN
+        with self.app.test_request_context('/validate/check',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "pass": pin}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            detail = res.json.get("detail")
+            self.assertFalse(result.get("value"))
+            self.assertEqual(detail.get("message"), _("please enter otp: "))
+            transaction_id = detail.get("transaction_id")
+
+        # delete the token
+        remove_token(serial=serial)
+        delete_policy("pol_chal_resp")
+
     def test_11b_challenge_response_multiple_hotp_failcounters(self):
         # Check behavior of Challenge-Response with multiple tokens
         # set a chalresp policy for HOTP
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
@@ -540,15 +540,14 @@ def test_18_challenges(self):
         transaction_id = "123456789"
 
         db_token.set_pin("test")
-        # No challenge request
-        req = token.is_challenge_request("test", User(login="cornelius",
+        # No challenge request, since we have the wrong pin
+        req = token.is_challenge_request("testX", User(login="cornelius",
                                                       realm=self.realm1))
         self.assertFalse(req, req)
-        # A challenge request
+        # A challenge request, since we have the correct pin
         req = token.is_challenge_request("test",
                                          User(login="cornelius",
-                                              realm=self.realm1),
-                                         {"data": "a challenge"})
+                                              realm=self.realm1))
         self.assertTrue(req, req)
 
         resp = token.is_challenge_response(User(login="cornelius",