Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Using Twilio
"sms_twilio": {
VERBOSE_NAME: _("sms_twilio"),
VALIDITY_PERIOD: 30,
HANDLER: "trench.backends.twilio.TwilioMessageDispatcher",
HANDLER: "trench.backends.twilio.TwilioSMSMessageDispatcher",
SOURCE_FIELD: "phone_number",
TWILIO_VERIFIED_FROM_NUMBER: "+48 123 456 789",
},
Expand Down Expand Up @@ -86,6 +86,33 @@ Using SMS API
:SMSAPI_ACCESS_TOKEN: Access token obtained from `SMS API`_
:SMSAPI_FROM_NUMBER: This will be used as the sender's phone number.

Phone call
**********

| Phone call backend make call with `Twilio`_ . Credentials can be set in method's specific settings.

Using Twilio
------------

| If you are using Twilio service for calling then you need to set ``TWILIO_ACCOUNT_SID`` and ``TWILIO_AUTH_TOKEN`` environment variables for Twilio API client to be used as credentials.

.. code-block:: python

TRENCH_AUTH = {
"MFA_METHODS": {
"call_twilio": {
VERBOSE_NAME: _("call_twilio"),
VALIDITY_PERIOD: 30,
HANDLER: "trench.backends.twilio.TwilioCallMessageDispatcher",
SOURCE_FIELD: "phone_number",
TWILIO_VERIFIED_FROM_NUMBER: "+48 123 456 789",
},
},
}

:SOURCE_FIELD: Defines the field name in your ``AUTH_USER_MODEL`` to be looked up and used as field containing the phone number of the recipient of the OTP code.
:TWILIO_VERIFIED_FROM_NUMBER: This will be used as the sender's phone number. Note: this number must be verified in the Twilio's client panel.

Authentication apps
*******************
| This backend returns OTP based QR link to be scanned by apps like Gooogle Authenticator and Authy.
Expand Down
12 changes: 11 additions & 1 deletion testproject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,20 @@
"BACKUP_CODES_QUANTITY": 8,
"DEFAULT_VALIDITY_PERIOD": 60,
"MFA_METHODS": {
"call_twilio": {
"VERBOSE_NAME": "call",
"VALIDITY_PERIOD": 60,
"HANDLER": "trench.backends.twilio.TwilioCallMessageDispatcher",
"SOURCE_FIELD": "phone_number",
"TWILIO_VERIFIED_FROM_NUMBER": env(
"TWILIO_VERIFIED_FROM_NUMBER",
default="",
),
},
"sms_twilio": {
"VERBOSE_NAME": "sms",
"VALIDITY_PERIOD": 60,
"HANDLER": "trench.backends.twilio.TwilioMessageDispatcher",
"HANDLER": "trench.backends.twilio.TwilioSMSMessageDispatcher",
"SOURCE_FIELD": "phone_number",
"TWILIO_VERIFIED_FROM_NUMBER": env(
"TWILIO_VERIFIED_FROM_NUMBER",
Expand Down
15 changes: 14 additions & 1 deletion testproject/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def active_user_with_sms_otp() -> UserModel:


@pytest.fixture()
def active_user_with_twilio_otp() -> UserModel:
def active_user_with_twilio_sms_otp() -> UserModel:
user, created = User.objects.get_or_create(
username="imhotep", email="imhotep@pyramids.eg", phone_number="555-555-555"
)
Expand All @@ -107,6 +107,19 @@ def active_user_with_twilio_otp() -> UserModel:
return user


@pytest.fixture()
def active_user_with_twilio_call_otp() -> UserModel:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code inside this function and here: active_user_with_twilio_sms_otp are the same instead of method_name. maybe extracting it to another function will avoid DRY?

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="call_twilio")
return user


@pytest.fixture()
def active_user_with_email_and_inactive_other_methods_otp() -> UserModel:
user, created = User.objects.get_or_create(
Expand Down
35 changes: 28 additions & 7 deletions testproject/tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from trench.backends.application import ApplicationMessageDispatcher
from trench.backends.sms_api import SMSAPIMessageDispatcher
from trench.backends.twilio import TwilioMessageDispatcher
from trench.backends.twilio import TwilioSMSMessageDispatcher, TwilioCallMessageDispatcher
from trench.backends.yubikey import YubiKeyMessageDispatcher
from trench.exceptions import MissingConfigurationError

Expand All @@ -13,10 +13,20 @@


@pytest.mark.django_db
def test_twilio_backend_without_credentials(active_user_with_twilio_otp, settings):
auth_method = active_user_with_twilio_otp.mfa_methods.get(name="sms_twilio")
def test_twilio_sms_backend_without_credentials(active_user_with_twilio_sms_otp, settings):
auth_method = active_user_with_twilio_sms_otp.mfa_methods.get(name="sms_twilio")
conf = settings.TRENCH_AUTH["MFA_METHODS"]["sms_twilio"]
response = TwilioMessageDispatcher(
response = TwilioSMSMessageDispatcher(
mfa_method=auth_method, config=conf
).dispatch_message()
assert response.data.get("details")[:23] == "Unable to create record"


@pytest.mark.django_db
def test_twilio_call_backend_without_credentials(active_user_with_twilio_call_otp, settings):
auth_method = active_user_with_twilio_call_otp.mfa_methods.get(name="call_twilio")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about storing mfa_methods in Enum class?

conf = settings.TRENCH_AUTH["MFA_METHODS"]["call_twilio"]
response = TwilioSMSMessageDispatcher(
mfa_method=auth_method, config=conf
).dispatch_message()
assert response.data.get("details")[:23] == "Unable to create record"
Expand Down Expand Up @@ -46,16 +56,27 @@ def test_sms_api_backend_with_wrong_credentials(active_user_with_sms_otp, settin


@pytest.mark.django_db
def test_sms_backend_misconfiguration_error(active_user_with_twilio_otp, settings):
auth_method = active_user_with_twilio_otp.mfa_methods.get(name="sms_twilio")
def test_twilio_sms_backend_misconfiguration_error(active_user_with_twilio_sms_otp, settings):
auth_method = active_user_with_twilio_sms_otp.mfa_methods.get(name="sms_twilio")
conf = settings.TRENCH_AUTH["MFA_METHODS"]["sms_twilio"]
current_source = settings.TRENCH_AUTH["MFA_METHODS"]["sms_twilio"]["SOURCE_FIELD"]
settings.TRENCH_AUTH["MFA_METHODS"]["sms_twilio"]["SOURCE_FIELD"] = "invalid.source"
with pytest.raises(MissingConfigurationError):
SMSAPIMessageDispatcher(mfa_method=auth_method, config=conf).dispatch_message()
TwilioSMSMessageDispatcher(mfa_method=auth_method, config=conf).dispatch_message()
settings.TRENCH_AUTH["MFA_METHODS"]["sms_twilio"]["SOURCE_FIELD"] = current_source


@pytest.mark.django_db
def test_twilio_call_backend_misconfiguration_error(active_user_with_twilio_call_otp, settings):
auth_method = active_user_with_twilio_call_otp.mfa_methods.get(name="call_twilio")
conf = settings.TRENCH_AUTH["MFA_METHODS"]["call_twilio"]
current_source = settings.TRENCH_AUTH["MFA_METHODS"]["call_twilio"]["SOURCE_FIELD"]
settings.TRENCH_AUTH["MFA_METHODS"]["call_twilio"]["SOURCE_FIELD"] = "invalid.source"
with pytest.raises(MissingConfigurationError):
TwilioCallMessageDispatcher(mfa_method=auth_method, config=conf).dispatch_message()
settings.TRENCH_AUTH["MFA_METHODS"]["call_twilio"]["SOURCE_FIELD"] = current_source


@pytest.mark.django_db
def test_application_backend_generating_url_successfully(
active_user_with_application_otp, settings
Expand Down
23 changes: 22 additions & 1 deletion trench/backends/twilio.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
SuccessfulDispatchResponse,
)
from trench.settings import TWILIO_VERIFIED_FROM_NUMBER
from twilio.twiml.voice_response import VoiceResponse


class TwilioMessageDispatcher(AbstractMessageDispatcher):
class TwilioSMSMessageDispatcher(AbstractMessageDispatcher):
_SMS_BODY = _("Your verification code is: ")
_SUCCESS_DETAILS = _("SMS message with MFA code has been sent.")

Expand All @@ -29,3 +30,23 @@ def dispatch_message(self) -> DispatchResponse:
except TwilioRestException as cause:
logging.error(cause, exc_info=True)
return FailedDispatchResponse(details=cause.msg)


class TwilioCallMessageDispatcher(AbstractMessageDispatcher):
_CALL_MESSAGE = _("Your verification code is: ")
_SUCCESS_DETAILS = _("Pick up phone and get key.")

def dispatch_message(self) -> DispatchResponse:
try:
response = VoiceResponse()
response.say(self._CALL_MESSAGE + self.create_code())
client = Client()
client.calls.create(
twiml=str(response),
to=self._to,
from_=self._config.get(TWILIO_VERIFIED_FROM_NUMBER),
)
return SuccessfulDispatchResponse(details=self._SUCCESS_DETAILS)
except TwilioRestException as cause:
logging.error(cause, exc_info=True)
return FailedDispatchResponse(details=cause.msg)
9 changes: 8 additions & 1 deletion trench/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,17 @@ def __getitem__(self, attr: str) -> Any:
"ENCRYPT_BACKUP_CODES": True,
"APPLICATION_ISSUER_NAME": "MyApplication",
"MFA_METHODS": {
"call_twilio": {
VERBOSE_NAME: _("call_twilio"),
VALIDITY_PERIOD: 30,
HANDLER: "trench.backends.twilio.TwilioCallMessageDispatcher",
SOURCE_FIELD: "phone_number",
TWILIO_VERIFIED_FROM_NUMBER: "YOUR TWILIO REGISTERED NUMBER",
},
"sms_twilio": {
VERBOSE_NAME: _("sms_twilio"),
VALIDITY_PERIOD: 30,
HANDLER: "trench.backends.twilio.TwilioMessageDispatcher",
HANDLER: "trench.backends.twilio.TwilioSMSMessageDispatcher",
SOURCE_FIELD: "phone_number",
TWILIO_VERIFIED_FROM_NUMBER: "YOUR TWILIO REGISTERED NUMBER",
},
Expand Down