Repository: privacyidea/privacyidea

Issue:
Challenge Response handling at /auth endpoint not consistent
Hi,
we are trying to implement 2fa in smartcard-client which is using `POST /auth` endpoint to authenticate in privacyIDEA.

Scenario: The user is sending just a PIN without OTP and awaits, that privacyIDEA will ask for the OTP in the next step.
Response: The server triggers the challenges and asks for OTP in the message, but at the same time it encounters an error: 4031.

The PI response looks as follows:

~~~~json

 "detail": { 
       "client_mode": "interactive", 
       "loginmode": "privacyIDEA", 
        "message": "please enter otp: ", 
        "messages": [ "please enter otp: " ], 
        "multi_challenge": [ 
                      { "client_mode": "interactive", "message": "please enter otp: ", "serial": "OATH00159D24", "transaction_id": "13606...996141", "type": "hotp" } ], 
           "serial": "OATH0...9D24", 
             "threadid": 13999...8608, 
             "transaction_id": "136062...90996141", 
             "transaction_ids": [ "136062...0996141" ], 
              "type": "hotp" }, 
"id": 1, 
"jsonrpc": "2.0", 
"result": { 
       "error": { "code": 4031, "message": "Authentication failure. Wrong credentials" }, 
       "status": false },`
~~~~

https://github.com/privacyidea/smartcard-client-windows/issues/6


Discussion:
Actually, challenge-response works with the `/auth` endpoint quite well. Just ignore the error and send the OTP value with a second `/auth` request and the the `transaction_id`.
The Problem is, that we throw an error if we do not have a proper authentication at the end of the `/auth` request.
There is already an old issue open: #1192

@lukasmatusiewicz 
The changes will be more complex. We will do this in 3.9.
Please use @plettich 's hint with ignoring the error for now.
Basically we would need to return the result (with details) in case we have challenges in the details before throwing the `AuthError` here:
https://github.com/privacyidea/privacyidea/blob/da4e3786acb7be0c6e9e9a04e382ab51d1863ae8/privacyidea/api/auth.py#L343
We could add a `return send_result()` like this:
https://github.com/privacyidea/privacyidea/blob/da4e3786acb7be0c6e9e9a04e382ab51d1863ae8/privacyidea/api/auth.py#L384
inside the `else` block.

The UI needs to be updated for the changed `/auth` response:
Most of the code in the whole error handling function 
https://github.com/privacyidea/privacyidea/blob/13565489141f81b61e8b9803df9358741d9269f0/privacyidea/static/components/login/controllers/loginControllers.js#L241
needs to go into the regular response handling here:
https://github.com/privacyidea/privacyidea/blob/13565489141f81b61e8b9803df9358741d9269f0/privacyidea/static/components/login/controllers/loginControllers.js#L236
Only the *show error message* code should remain in the error handler:
https://github.com/privacyidea/privacyidea/blob/13565489141f81b61e8b9803df9358741d9269f0/privacyidea/static/components/login/controllers/loginControllers.js#L356

Resolving patch:
diff --git a/privacyidea/api/auth.py b/privacyidea/api/auth.py
--- a/privacyidea/api/auth.py
+++ b/privacyidea/api/auth.py
@@ -340,6 +340,12 @@ def get_auth_token():
                                     "info": "{0!s}|loginmode={1!s}".format(log_used_user(user_obj),
                                                                            details.get("loginmode"))})
 
+            if not user_auth and "multi_challenge" in details and len(details["multi_challenge"]) > 0:
+                return send_result({"role": role,
+                                    "username": loginname,
+                                    "realm": realm},
+                                   details=details)
+
     if not admin_auth and not user_auth:
         raise AuthError(_("Authentication failure. Wrong credentials"),
                         id=ERROR.AUTHENTICATE_WRONG_CREDENTIALS,


Test patch:
diff --git a/tests/test_api_auth.py b/tests/test_api_auth.py
--- a/tests/test_api_auth.py
+++ b/tests/test_api_auth.py
@@ -786,9 +786,9 @@ def test_01_setup_eventhandlers(self):
                                            data={"username": "someuser",
                                                  "password": "test"}):
             res = self.app.full_dispatch_request()
-            self.assertEqual(401, res.status_code, res)
+            self.assertEqual(200, res.status_code, res)
             result = res.json.get("result")
-            self.assertFalse(result.get("status"), result)
+            self.assertTrue(result.get("status"), result)
             detail = res.json.get("detail")
             self.assertEqual("please enter otp: ", detail.get("message"))
             transaction_id = detail.get("transaction_id")
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
@@ -186,9 +186,9 @@ def test_01_challenge_response_at_webui(self):
                                            data={"username": "selfservice",
                                                  "password": "pin"}):
             res = self.app.full_dispatch_request()
-            self.assertTrue(res.status_code == 401, res)
+            self.assertTrue(res.status_code == 200, res)
             data = res.json
-            self.assertFalse(data.get("result").get("status"))
+            self.assertTrue(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")
@@ -215,7 +215,7 @@ def test_02_auth_chal_resp_multi_token(self):
                                            data={"username": "selfservice",
                                                  "password": 'test'}):
             res = self.app.full_dispatch_request()
-            self.assertEqual(res.status_code, 401, res)
+            self.assertEqual(res.status_code, 200, res)
             result = res.json.get('detail')
             self.assertTrue('multi_challenge' in result, result)
             self.assertEqual({'hotp1', 'hotp2'}, set([x['serial'] for x in result.get('multi_challenge')]))
diff --git a/tests/test_lib_tokens_push.py b/tests/test_lib_tokens_push.py
--- a/tests/test_lib_tokens_push.py
+++ b/tests/test_lib_tokens_push.py
@@ -880,10 +880,10 @@ def test_06_api_auth(self):
                                                      PUSH_ACTION.WAIT: "10",
                                                      "password": "pushpin"}):
                 res = self.app.full_dispatch_request()
-                self.assertEqual(res.status_code, 401)
+                self.assertEqual(res.status_code, 200)
                 jsonresp = res.json
-                self.assertFalse(jsonresp.get("result").get("value"))
-                self.assertFalse(jsonresp.get("result").get("status"))
+                self.assertTrue(jsonresp.get("result").get("value"))
+                self.assertTrue(jsonresp.get("result").get("status"))
                 self.assertEqual(jsonresp.get("detail").get("serial"), tokenobj.token.serial)
                 self.assertIn("transaction_id", jsonresp.get("detail"))
                 transaction_id = jsonresp.get("detail").get("transaction_id")