Repository: privacyidea/privacyidea

Issue:
Response when sending SMS fails
We use /validate/check API call and sometimes we receive response that The PIN was correct, but the SMS could not be sent, which comes from this part of code:

https://github.com/privacyidea/privacyidea/blob/5156a46e94b9b7c4a413719e7c887756a4ccf657/privacyidea/lib/tokens/smstoken.py#L306

This call returns 200OK status code, which is not correct and we cannot handle properly this error (except we can parse detail, but this is not correct way of detecting errors)





Discussion:
Do I understand correctly that you think there should be another HTTP response code?
yup, correct
What do you think about such response?

This could be easily implemented without breaking anything when passing the additional parameter "exception=1", which would mean, "give me the error as an exception".

~~~json
% http POST http://localhost:5000/validate/check user=daemon pass=sms exception=1
HTTP/1.0 500 INTERNAL SERVER ERROR
Cache-Control: no-cache
Content-Length: 922
Content-Type: application/json
Date: Tue, 21 Aug 2018 06:17:10 GMT
Server: Werkzeug/0.14.1 Python/2.7.12

{
    "detail": null, 
    "id": 1, 
    "jsonrpc": "2.0", 
    "result": {
        "error": {
            "code": -500, 
            "message": "The PIN was correct, but the SMS could not be sent: SMSError(error_id=404, description='SMS could not be sent: 404')"
        }, 
        "status": false
    }, 
    "signature": "1758587108677527432478244048728797748611158940433852191808571239201044832421942911266301848406441250607213611956425529554433442255274518376634008689948698941495098516251724420823921017095172849336872791487776980436709771121726646549289711882881549927070261384345478346164631637118972949011738951761302707076981015263159762824067553483470982777553338982903047802370270770185543957429367499820980465710381605418892544544615186216303081447283606018669573531668160755785299655247802820433124877165381971440716046485236649610422466830535934363246317675467432302912614979104008598931632306189102342068963993078549580716781", 
    "time": 1534832230.5643, 
    "version": "privacyIDEA 2.23.dev7"
}
~~~~


Resolving patch:
diff --git a/privacyidea/api/validate.py b/privacyidea/api/validate.py
--- a/privacyidea/api/validate.py
+++ b/privacyidea/api/validate.py
@@ -230,6 +230,10 @@ def check():
     The PIN and OTP value is sent in the parameter ``pass``.
     In case of successful authentication it returns ``result->value: true``.
 
+    In case of a challenge response authentication a parameter ``exception=1``
+    can be passed. This would result in a HTTP 500 Server Error response if
+    an error occurred during sending of SMS or Email.
+
     In case ``/validate/radiuscheck`` is requested, the responses are
     modified as follows: A successful authentication returns an empty HTTP
     204 response. An unsuccessful authentication returns an empty HTTP
diff --git a/privacyidea/lib/tokens/emailtoken.py b/privacyidea/lib/tokens/emailtoken.py
--- a/privacyidea/lib/tokens/emailtoken.py
+++ b/privacyidea/lib/tokens/emailtoken.py
@@ -1,4 +1,7 @@
 # -*- coding: utf-8 -*-
+#  2018-10-31 Cornelius Kölbel <cornelius.koelbel@netknights.it>
+#             Let the client choose to get a HTTP 500 Error code if
+#             SMS fails.
 #  2018-02-16 Cornelius Kölbel <cornelius.koelbel@netknights.it>
 #             Add dynamic email address. Dynamically read from user source.
 #  2018-01-21 Cornelius Kölbel <cornelius.koelbel@netknights.it>
@@ -235,6 +238,8 @@ def create_challenge(self, transactionid=None, options=None):
 
         :param transactionid: the id of this challenge
         :param options: the request context parameters / data
+                You can pass exception=1 to raise an exception, if
+                the SMS could not be sent. Otherwise the message is contained in the response.
         :return: tuple of (bool, message and data)
                  bool, if submit was successful
                  message is submitted to the user
@@ -284,6 +289,8 @@ def create_challenge(self, transactionid=None, options=None):
                 log.warning(info)
                 log.debug(u"{0!s}".format(traceback.format_exc(e)))
                 return_message = info
+                if is_true(options.get("exception")):
+                    raise Exception(info)
 
         expiry_date = datetime.datetime.now() + \
                                     datetime.timedelta(seconds=validity)
diff --git a/privacyidea/lib/tokens/smstoken.py b/privacyidea/lib/tokens/smstoken.py
--- a/privacyidea/lib/tokens/smstoken.py
+++ b/privacyidea/lib/tokens/smstoken.py
@@ -5,6 +5,9 @@
 #  License:  AGPLv3
 #  contact:  http://www.privacyidea.org
 #
+#  2018-10-31   Cornelius Kölbel <cornelius.koelbel@netknights.it>
+#               Let the client choose to get a HTTP 500 Error code if
+#               SMS fails.
 #  2018-02-16   Cornelius Kölbel <cornelius.koelbel@netknights.it>
 #               Allow to use a dynamic_phone
 #  2016-06-20   Cornelius Kölbel <cornelius.koelbel@netkngihts.it>
@@ -62,7 +65,8 @@
 from privacyidea.models import Challenge
 from privacyidea.lib.decorators import check_token_locked
 import logging
-from privacyidea.lib.policydecorators import challenge_response_allowed
+
+
 log = logging.getLogger(__name__)
 
 keylen = {'sha1': 20,
@@ -266,6 +270,8 @@ def create_challenge(self, transactionid=None, options=None):
 
         :param transactionid: the id of this challenge
         :param options: the request context parameters / data
+                You can pass exception=1 to raise an exception, if
+                the SMS could not be sent. Otherwise the message is contained in the response.
         :return: tuple of (bool, message and data)
                  bool, if submit was successful
                  message is submitted to the user
@@ -308,6 +314,8 @@ def create_challenge(self, transactionid=None, options=None):
                 log.warning(info)
                 log.debug("{0!s}".format(traceback.format_exc()))
                 return_message = info
+                if is_true(options.get("exception")):
+                    raise Exception(info)
 
         expiry_date = datetime.datetime.now() + \
                                     datetime.timedelta(seconds=validity)


Test patch:
diff --git a/tests/test_lib_tokens_email.py b/tests/test_lib_tokens_email.py
--- a/tests/test_lib_tokens_email.py
+++ b/tests/test_lib_tokens_email.py
@@ -459,6 +459,8 @@ def test_20_sending_email_exception(self):
         self.assertFalse(c[0])
         self.assertTrue("The PIN was correct, but the EMail could not "
                         "be sent" in c[1])
+        # test with the parameter exception=1
+        self.assertRaises(Exception, token.create_challenge, transactionid, {"exception": "1"})
 
     @smtpmock.activate
     def test_21_test_email_config(self):
diff --git a/tests/test_lib_tokens_sms.py b/tests/test_lib_tokens_sms.py
--- a/tests/test_lib_tokens_sms.py
+++ b/tests/test_lib_tokens_sms.py
@@ -488,3 +488,7 @@ def test_21_failed_loading(self):
         c = token.create_challenge(transactionid)
         self.assertFalse(c[0], c)
         self.assertTrue("Failed to load sms.providerConfig" in c[1], c[1])
+
+        # test with the parameter exception=1
+        self.assertRaises(Exception, token.create_challenge, transactionid, {"exception": "1"})
+