Repository: merixstudio/django-trench (language: python)

Issue:
AWS support for SMS
**Is your feature request related to a problem? Please describe.**
Can not send SMS through AWS SNS.

**Describe the solution you'd like**
Being able to send SMS through AWS SNS.

**Describe alternatives you've considered**
Other SMS providers.

**Additional context**
None


Discussion / hints:

Fix patch:
diff --git a/setup.py b/setup.py
index 98cfeda2..8c699f6c 100644
--- a/setup.py
+++ b/setup.py
@@ -19,6 +19,7 @@
         "pyotp>=2.6.0",
         "twilio>=6.56.0",
         "yubico-client>=1.13.0",
+        "boto3>=1.21.37",
         "smsapi-client>=2.4.5",
     ],
     extras_require={
diff --git a/trench/backends/aws.py b/trench/backends/aws.py
new file mode 100644
index 00000000..7d8cfaed
--- /dev/null
+++ b/trench/backends/aws.py
@@ -0,0 +1,38 @@
+from django.utils.translation import gettext_lazy as _
+
+import logging
+import boto3
+import botocore.exceptions
+
+from trench.backends.base import AbstractMessageDispatcher
+from trench.responses import (
+    DispatchResponse,
+    FailedDispatchResponse,
+    SuccessfulDispatchResponse,
+)
+from trench.settings import AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_REGION
+from botocore.exceptions import ClientError, EndpointConnectionError
+
+class AWSMessageDispatcher(AbstractMessageDispatcher):
+    _SMS_BODY = _("Your verification code is: ")
+    _SUCCESS_DETAILS = _("SMS message with MFA code has been sent.")
+
+    def dispatch_message(self) -> DispatchResponse:
+        try:
+            client = boto3.client(
+                "sns",
+                aws_access_key_id=self._config.get(AWS_ACCESS_KEY),
+                aws_secret_access_key=self._config.get(AWS_SECRET_KEY),
+                region_name=self._config.get(AWS_REGION),
+            )
+            client.publish(
+                PhoneNumber=self._to,
+                Message=self._SMS_BODY + self.create_code(),
+            )
+            return SuccessfulDispatchResponse(details=self._SUCCESS_DETAILS)
+        except ClientError as cause:
+            logging.error(cause, exc_info=True)
+            return FailedDispatchResponse(details=str(cause))
+        except EndpointConnectionError as cause:
+            logging.error(cause, exc_info=True)
+            return FailedDispatchResponse(details=str(cause))
diff --git a/trench/settings.py b/trench/settings.py
index 2a1bfc63..885fad5a 100644
--- a/trench/settings.py
+++ b/trench/settings.py
@@ -52,6 +52,9 @@ def __getitem__(self, attr: str) -> Any:
 SMSAPI_FROM_NUMBER = "SMSAPI_FROM_NUMBER"
 TWILIO_VERIFIED_FROM_NUMBER = "TWILIO_VERIFIED_FROM_NUMBER"
 YUBICLOUD_CLIENT_ID = "YUBICLOUD_CLIENT_ID"
+AWS_ACCESS_KEY = "AWS_ACCESS_KEY"
+AWS_SECRET_KEY = "AWS_SECRET_KEY"
+AWS_REGION = "AWS_REGION"
 
 DEFAULTS = {
     "USER_MFA_MODEL": "trench.MFAMethod",
@@ -82,6 +85,15 @@ def __getitem__(self, attr: str) -> Any:
             SMSAPI_ACCESS_TOKEN: "YOUR SMSAPI TOKEN",
             SMSAPI_FROM_NUMBER: "YOUR REGISTERED NUMBER",
         },
+        "sms_aws": {
+            VERBOSE_NAME: _("sms_aws"),
+            VALIDITY_PERIOD: 30,
+            HANDLER: "trench.backends.aws.AWSMessageDispatcher",
+            SOURCE_FIELD: "phone_number",
+            AWS_ACCESS_KEY: "YOUR AWS ACCESS KEY",
+            AWS_SECRET_KEY: "YOUR AWS SECRET KEY",
+            AWS_REGION: "YOUR AWS REGION",
+        },
         "email": {
             VERBOSE_NAME: _("email"),
             VALIDITY_PERIOD: 30,


Test patch:
diff --git a/testproject/requirements.txt b/testproject/requirements.txt
index 5e82a0b3..ac31eb58 100644
--- a/testproject/requirements.txt
+++ b/testproject/requirements.txt
@@ -1,3 +1,5 @@
+boto3==1.21.38
+botocore==1.24.38
 Django
 djangorestframework
 django-environ==0.4.5
diff --git a/testproject/settings.py b/testproject/settings.py
index b393c99b..ea447f7d 100644
--- a/testproject/settings.py
+++ b/testproject/settings.py
@@ -141,6 +141,15 @@
             "SMSAPI_ACCESS_TOKEN": "token",
             "SMSAPI_FROM_NUMBER": "123 456 789",
         },
+        "sms_aws": {
+            "VERBOSE_NAME": "sms_aws",
+            "VALIDITY_PERIOD": 30,
+            "HANDLER": "trench.backends.aws.AWSMessageDispatcher",
+            "SOURCE_FIELD": "phone_number",
+            "AWS_ACCESS_KEY": "access_key",
+            "AWS_SECRET_KEY": "secret_key",
+            "AWS_REGION": "region",
+        },
         "email": {
             "VERBOSE_NAME": "email",
             "VALIDITY_PERIOD": 60,
diff --git a/testproject/tests/conftest.py b/testproject/tests/conftest.py
index b75c4c0e..654a1fd5 100644
--- a/testproject/tests/conftest.py
+++ b/testproject/tests/conftest.py
@@ -107,6 +107,19 @@ def active_user_with_twilio_otp() -> UserModel:
     return user
 
 
+@pytest.fixture()
+def active_user_with_sms_aws_otp() -> UserModel:
+    user, created = User.objects.get_or_create(
+        username="imhotep", email="imhotep@pyramids.eg", phone_number="555-555-555"
+    )
+    if created:
+        user.set_password("secretkey"),
+        user.is_active = True
+        user.save()
+        mfa_method_creator(user=user, method_name="sms_aws")
+    return user
+
+
 @pytest.fixture()
 def active_user_with_email_and_inactive_other_methods_otp() -> UserModel:
     user, created = User.objects.get_or_create(
diff --git a/testproject/tests/test_backends.py b/testproject/tests/test_backends.py
index 182e4c33..981511e3 100644
--- a/testproject/tests/test_backends.py
+++ b/testproject/tests/test_backends.py
@@ -3,6 +3,7 @@
 from django.contrib.auth import get_user_model
 
 from trench.backends.application import ApplicationMessageDispatcher
+from trench.backends.aws import AWSMessageDispatcher
 from trench.backends.sms_api import SMSAPIMessageDispatcher
 from trench.backends.twilio import TwilioMessageDispatcher
 from trench.backends.yubikey import YubiKeyMessageDispatcher
@@ -78,3 +79,13 @@ def test_yubikey_backend(active_user_with_many_otp_methods, settings):
     auth_method = user.mfa_methods.get(name="yubi")
     dispatcher = YubiKeyMessageDispatcher(mfa_method=auth_method, config=config)
     dispatcher.confirm_activation(code)
+
+
+@pytest.mark.django_db
+def test_sms_aws_backend_without_credentials(active_user_with_sms_aws_otp, settings):
+    auth_method = active_user_with_sms_aws_otp.mfa_methods.get(name="sms_aws")
+    conf = settings.TRENCH_AUTH["MFA_METHODS"]["sms_aws"]
+    response = AWSMessageDispatcher(
+        mfa_method=auth_method, config=conf
+    ).dispatch_message()
+    assert response.data.get("details")[:38] == "Could not connect to the endpoint URL:"