Repository: privacyidea/privacyidea

Issue:
SMS error message appended to return information
When an error occurs while sending an SMS (for SMS Token), the error message will be appended to the information returned to the client. This might get displayed to the user and thus disclose internal configuration:
https://github.com/privacyidea/privacyidea/blob/db7cb708e16e5226cdcc17e346e0eb65fd8f1b0e/privacyidea/lib/tokens/smstoken.py#L350
The same is true for the Email-Token.
We might want to reduce the information send/presented to the user and just log the error message in the internal log.



Discussion:
I think the solution is rather simple:

Writing the message to the log makes sense here:
https://github.com/privacyidea/privacyidea/blob/db7cb708e16e5226cdcc17e346e0eb65fd8f1b0e/privacyidea/lib/tokens/smstoken.py#L353

The `return_message` that is the return value of the method should only be "The PIN was correct, but the SMS could not be sent."

This can be tested with email more easily.

Resolving patch:
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
@@ -252,7 +252,7 @@ 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.
+            the Email could not be sent.
         :return: tuple
             of (success, message, transactionid, attributes)
 
@@ -304,9 +304,9 @@ def create_challenge(self, transactionid=None, options=None):
                     mimetype=mimetype)
 
             except Exception as e:
-                info = ("The PIN was correct, but the "
-                        "EMail could not be sent: %r" % e)
-                log.warning(info)
+                info = _("The PIN was correct, but the "
+                         "EMail could not be sent!")
+                log.warning(info + " ({0!s})".format(e))
                 log.debug(u"{0!s}".format(traceback.format_exc()))
                 return_message = info
                 if is_true(options.get("exception")):
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
@@ -306,7 +306,7 @@ 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.
+                the SMS could not be sent.
         :return: tuple of (bool, message and data)
                  bool, if submit was successful
                  message is submitted to the user
@@ -349,9 +349,9 @@ def create_challenge(self, transactionid=None, options=None):
                 db_challenge.save()
                 transactionid = transactionid or db_challenge.transaction_id
             except Exception as e:
-                info = ("The PIN was correct, but the "
-                        "SMS could not be sent: %r" % e)
-                log.warning(info)
+                info = _("The PIN was correct, but the "
+                         "SMS could not be sent!")
+                log.warning(info + " ({0!s})".format(e))
                 log.debug("{0!s}".format(traceback.format_exc()))
                 return_message = info
                 if is_true(options.get("exception")):


Test patch:
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
@@ -1,14 +1,14 @@
 """
 This test file tests the lib.tokens.smstoken
 """
-PWFILE = "tests/testdata/passwords"
+import logging
 
 from .base import MyTestCase, FakeFlaskG, FakeAudit
 from privacyidea.lib.resolver import (save_resolver)
 from privacyidea.lib.realm import (set_realm)
 from privacyidea.lib.user import (User)
 from privacyidea.lib.utils import is_true
-from privacyidea.lib.tokenclass import DATE_FORMAT
+from privacyidea.lib.token import init_token, remove_token
 from privacyidea.lib.tokens.smstoken import SmsTokenClass, SMSACTION
 from privacyidea.models import (Token, Config, Challenge)
 from privacyidea.lib.config import (set_privacyidea_config, set_prepend_pin)
@@ -16,7 +16,11 @@
 from privacyidea.lib import _
 import datetime
 import mock
+import six
 import responses
+from testfixtures import log_capture
+
+PWFILE = "tests/testdata/passwords"
 
 
 class SMSTokenTestCase(MyTestCase):
@@ -472,26 +476,48 @@ def test_19_smstext(self):
         r = token.check_otp(self.valid_otp_values[5 + len(smstext_tests)], options=options)
         self.assertTrue(r > 0, r)
 
-    def test_21_failed_loading(self):
+    @ log_capture(level=logging.WARN)
+    def test_21_failed_loading(self, capture):
+        token = init_token({'type': 'sms', 'phone': self.phone1})
         transactionid = "123456098712"
         set_privacyidea_config("sms.providerConfig", "noJSON")
         set_privacyidea_config("sms.provider",
                                "privacyidea.lib.smsprovider."
                                "HttpSMSProvider.HttpSMSProviderWRONG")
-        db_token = Token.query.filter_by(serial=self.serial1).first()
-        token = SmsTokenClass(db_token)
-
-        c = token.create_challenge(transactionid)
-        self.assertFalse(c[0], c)
-        self.assertTrue("The PIN was correct, but" in c[1], c[1])
 
-        set_privacyidea_config("sms.provider",
-                               "privacyidea.lib.smsprovider."
-                               "HttpSMSProvider.HttpSMSProvider")
-        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
+        with mock.patch("logging.Logger.error") as mock_log:
+            c = token.create_challenge(transactionid)
+            self.assertFalse(c[0], c)
+            self.assertTrue(c[1].startswith("The PIN was correct, but"), c[1])
+            if six.PY2:
+                expected = "Failed to load SMSProvider: ImportError" \
+                           "(u'privacyidea.lib.smsprovider.HttpSMSProvider has no attribute HttpSMSProviderWRONG',)"
+            else:
+                expected = "Failed to load SMSProvider: ImportError" \
+                           "('privacyidea.lib.smsprovider.HttpSMSProvider has no attribute HttpSMSProviderWRONG'"
+            mock_log.mock_called()
+            mocked_str = mock_log
+            self.assertTrue(mocked_str.startswith(expected), mocked_str)
+        capture.clear()
+
+        with mock.patch("logging.Logger.error") as mock_log:
+            set_privacyidea_config("sms.provider",
+                                   "privacyidea.lib.smsprovider."
+                                   "HttpSMSProvider.HttpSMSProvider")
+            c = token.create_challenge(transactionid)
+            self.assertFalse(c[0], c)
+            self.assertTrue(c[1].startswith("The PIN was correct, but"), c[1])
+            if six.PY2:
+                expected = "Failed to load sms.providerConfig: ValueError('No JSON object could be decoded',)"
+            else:
+                expected = "Failed to load sms.providerConfig: " \
+                           "JSONDecodeError('Expecting value: line 1 column 1 (char 0)')"
+            mock_log.mock_called()
+            mocked_str = mock_log
+            self.assertTrue(mocked_str.startswith(expected), mocked_str)
+        capture.clear()
+
+        #test with the parameter exception=1
         self.assertRaises(Exception, token.create_challenge, transactionid, {"exception": "1"})
 
+        remove_token(token.get_serial())
\ No newline at end of file