Repository: goauthentik/authentik

Issue:
Twilio SMS sending "U" instead of numerical MFA code
**Describe the bug**
When using the SMS authenticator stage with Twilio configured, users are being sent MFA text messages containing nothing other than the letter "U". I have confirmed in the Twilio console that the body of the message received from Authentik is indeed just the letter "U".

**To Reproduce**
Steps to reproduce the behavior:
1. Log in with a user that has an SMS device setup, or set up a new SMS device
2. See issue with received text message containing only the letter "U"

**Expected behavior**
Users should receive a text message with a numerical code for MFA.

**Version and Deployment (please complete the following information):**
 - authentik version: 2023.2.2
 - Deployment: docker-compose



Discussion:

Resolving patch:
diff --git a/authentik/stages/authenticator_sms/models.py b/authentik/stages/authenticator_sms/models.py
--- a/authentik/stages/authenticator_sms/models.py
+++ b/authentik/stages/authenticator_sms/models.py
@@ -86,7 +86,7 @@ def send_twilio(self, token: str, device: "SMSDevice"):
 
         try:
             message = client.messages.create(
-                to=device.phone_number, from_=self.from_number, body=self.get_message(token)
+                to=device.phone_number, from_=self.from_number, body=str(self.get_message(token))
             )
             LOGGER.debug("Sent SMS", to=device, message=message.sid)
         except TwilioRestException as exc:
@@ -115,13 +115,13 @@ def send_generic(self, token: str, device: "SMSDevice"):
 
         if self.auth_type == SMSAuthTypes.BEARER:
             response = get_http_session().post(
-                f"{self.account_sid}",
+                self.account_sid,
                 json=payload,
                 headers={"Authorization": f"Bearer {self.auth}"},
             )
         elif self.auth_type == SMSAuthTypes.BASIC:
             response = get_http_session().post(
-                f"{self.account_sid}",
+                self.account_sid,
                 json=payload,
                 auth=(self.auth, self.auth_password),
             )


Test patch:
diff --git a/authentik/stages/authenticator_sms/tests.py b/authentik/stages/authenticator_sms/tests.py
--- a/authentik/stages/authenticator_sms/tests.py
+++ b/authentik/stages/authenticator_sms/tests.py
@@ -1,17 +1,21 @@
 """Test SMS API"""
 from unittest.mock import MagicMock, patch
+from urllib.parse import parse_qsl
 
 from django.urls import reverse
+from requests_mock import Mocker
 
 from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 from authentik.flows.models import FlowStageBinding
 from authentik.flows.tests import FlowTestCase
+from authentik.lib.generators import generate_id
 from authentik.stages.authenticator_sms.models import (
     AuthenticatorSMSStage,
     SMSDevice,
     SMSProviders,
     hash_phone_number,
 )
+from authentik.stages.authenticator_sms.stage import SESSION_KEY_SMS_DEVICE
 
 
 class AuthenticatorSMSStageTests(FlowTestCase):
@@ -80,6 +84,61 @@ def test_stage_submit(self):
             phone_number_required=False,
         )
 
+    def test_stage_submit_twilio(self):
+        """test stage (submit) (twilio)"""
+        self.stage.account_sid = generate_id()
+        self.stage.auth = generate_id()
+        self.stage.from_number = generate_id()
+        self.stage.save()
+        self.client.get(
+            reverse("authentik_flows:configure", kwargs={"stage_uuid": self.stage.stage_uuid}),
+        )
+        response = self.client.get(
+            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
+        )
+        self.assertStageResponse(
+            response,
+            self.flow,
+            self.user,
+            component="ak-stage-authenticator-sms",
+            phone_number_required=True,
+        )
+        number = generate_id()
+
+        with Mocker() as mocker:
+            mocker.post(
+                (
+                    "https://api.twilio.com/2010-04-01/Accounts/"
+                    f"{self.stage.account_sid}/Messages.json"
+                ),
+                json={},
+            )
+            response = self.client.post(
+                reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
+                data={"component": "ak-stage-authenticator-sms", "phone_number": number},
+            )
+            self.assertEqual(response.status_code, 200)
+            self.assertEqual(mocker.call_count, 1)
+            self.assertEqual(mocker.request_history[0].method, "POST")
+            request_body = dict(parse_qsl(mocker.request_history[0].body))
+            device: SMSDevice = self.client.session[SESSION_KEY_SMS_DEVICE]
+            self.assertEqual(
+                request_body,
+                {
+                    "To": number,
+                    "From": self.stage.from_number,
+                    "Body": f"Use this code to authenticate in authentik: {device.token}",
+                },
+            )
+        self.assertStageResponse(
+            response,
+            self.flow,
+            self.user,
+            component="ak-stage-authenticator-sms",
+            response_errors={},
+            phone_number_required=False,
+        )
+
     def test_stage_context_data(self):
         """test stage context data"""
         self.client.get(