Repository: privacyidea/privacyidea

Issue:
Challenge-Response authentication at webUI does not work
Using a challenge-response method to authenticate at the webUI fails with "Authentication failure. Wrong credentials".




Discussion:

Resolving patch:
diff --git a/privacyidea/api/auth.py b/privacyidea/api/auth.py
--- a/privacyidea/api/auth.py
+++ b/privacyidea/api/auth.py
@@ -258,7 +258,8 @@ def get_auth_token():
 
     if not admin_auth and not user_auth:
         raise AuthError(_("Authentication failure. Wrong credentials"),
-                        id=ERROR.AUTHENTICATE_WRONG_CREDENTIALS)
+                        id=ERROR.AUTHENTICATE_WRONG_CREDENTIALS,
+                        details=details or {})
     else:
         g.audit_object.log({"success": True})
 
diff --git a/privacyidea/api/before_after.py b/privacyidea/api/before_after.py
--- a/privacyidea/api/before_after.py
+++ b/privacyidea/api/before_after.py
@@ -269,7 +269,8 @@ def auth_error(error):
         g.audit_object.log({"info": error.message})
         g.audit_object.finalize_log()
     return send_error(error.message,
-                      error_code=error.id), 401
+                      error_code=error.id,
+                      details=error.details), 401
 
 
 @system_blueprint.errorhandler(PolicyError)
diff --git a/privacyidea/lib/error.py b/privacyidea/lib/error.py
--- a/privacyidea/lib/error.py
+++ b/privacyidea/lib/error.py
@@ -109,7 +109,8 @@ def __repr__(self):
 
 
 class AuthError(privacyIDEAError):
-    def __init__(self, description, id=ERROR.AUTHENTICATE):
+    def __init__(self, description, id=ERROR.AUTHENTICATE, details=None):
+        self.details = details
         privacyIDEAError.__init__(self, description=description, id=id)
         
 


Test patch:
diff --git a/tests/base.py b/tests/base.py
--- a/tests/base.py
+++ b/tests/base.py
@@ -31,6 +31,17 @@ class MyTestCase(unittest.TestCase):
     realm3 = "realm3"
     serials = ["SE1", "SE2", "SE3"]
     otpkey = "3132333435363738393031323334353637383930"
+    valid_otp_values = ["755224",
+                        "287082",
+                        "359152",
+                        "969429",
+                        "338314",
+                        "254676",
+                        "287922",
+                        "162583",
+                        "399871",
+                        "520489"]
+
 
     @classmethod
     def setUpClass(cls):
diff --git a/tests/test_api_roles.py b/tests/test_api_roles.py
--- a/tests/test_api_roles.py
+++ b/tests/test_api_roles.py
@@ -145,6 +145,42 @@ def test_03_realmadmin_get_user(self):
         delete_policy("realmadmin")
 
 
+class APIAuthChallengeResponse(MyTestCase):
+
+    def setUp(self):
+        self.setUp_user_realms()
+        # New token for the user "selfservice"
+        Token("hotp1", "hotp", otpkey=self.otpkey, userid=1004, resolver=self.resolvername1,
+              realm=self.realm1).save()
+        # Define HOTP token to be challenge response
+        set_policy(name="pol_cr", scope=SCOPE.AUTH, action="{0!s}=hotp".format(ACTION.CHALLENGERESPONSE))
+        set_policy(name="webuilog", scope=SCOPE.WEBUI, action="{0!s}=privacyIDEA".format(ACTION.LOGINMODE))
+        from privacyidea.lib.token import set_pin
+        set_pin("hotp1", "pin")
+
+    def test_01_challenge_response_at_webui(self):
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "selfservice",
+                                                 "password": "pin"}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 401, res)
+            data = json.loads(res.data)
+            self.assertFalse(data.get("result").get("status"))
+            detail = data.get("detail")
+            self.assertTrue("enter otp" in detail.get("message"), detail.get("message"))
+            transaction_id = detail.get("transaction_id")
+
+        # Now we try to login with the OTP value
+        with self.app.test_request_context('/auth',
+                                           method='POST',
+                                           data={"username": "selfservice",
+                                                 "password": self.valid_otp_values[0],
+                                                 "transaction_id": transaction_id}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            data = json.loads(res.data)
+            self.assertEqual(data.get("result").get("value").get("username"), "selfservice")
 
 
 class APISelfserviceTestCase(MyTestCase):
@@ -574,7 +610,6 @@ def test_08_user_can_set_pin(self):
             self.assertTrue(response.get("result").get("value"),
                             response.get("result"))
 
-
     def test_31_user_is_not_allowed_for_some_api_calls(self):
         self.authenticate_selfservice_user()
         serial = "serial0001"