diff --git a/docs/endpoints.rst b/docs/endpoints.rst index b3e2bbda..dd1f9b36 100644 --- a/docs/endpoints.rst +++ b/docs/endpoints.rst @@ -17,6 +17,14 @@ MFA method activation * - Parameters - ``:method_name`` - **required** - Allowed method names: ``email``, ``app``, ``yubi``, ``sms_api``, ``sms_twilio`` + * - Payload + - .. code-block:: json + + { + "code": "123456" + } + + - ``code`` - If the user has an active base method, he must provide the code from that method * - Successful response - .. code-block:: json diff --git a/testproject/tests/test_second_step_authentication.py b/testproject/tests/test_second_step_authentication.py index aecf3b3b..7c030b74 100644 --- a/testproject/tests/test_second_step_authentication.py +++ b/testproject/tests/test_second_step_authentication.py @@ -21,7 +21,6 @@ from trench.exceptions import MFAMethodDoesNotExistError from trench.models import MFAMethod - User = get_user_model() @@ -139,11 +138,15 @@ def test_second_method_activation(active_user_with_email_otp): client.authenticate_multi_factor( mfa_method=mfa_method, user=active_user_with_email_otp ) + handler = get_mfa_handler(mfa_method=mfa_method) assert len(active_user_with_email_otp.mfa_methods.all()) == 1 try: client.post( path="/auth/sms_twilio/activate/", - data={"phone_number": "555-555-555"}, + data={ + "phone_number": "555-555-555", + "code": handler.create_code() + }, format="json", ) except TwilioException: @@ -458,10 +461,15 @@ def test_confirm_activation_otp_with_backup_code( ) assert response.status_code == HTTP_200_OK client._update_jwt_from_response(response=response) + mfa_method = active_user.mfa_methods.first() + handler = get_mfa_handler(mfa_method=mfa_method) try: client.post( path="/auth/sms_twilio/activate/", - data={"phone_number": "555-555-555"}, + data={ + "phone_number": "555-555-555", + "code": handler.create_code() + }, format="json", ) except (TwilioRestException, TwilioException): diff --git a/trench/models.py b/trench/models.py index 0e741a29..5225ce6e 100644 --- a/trench/models.py +++ b/trench/models.py @@ -14,7 +14,7 @@ ) from django.utils.translation import gettext_lazy as _ -from typing import Any, Iterable +from typing import Any, Iterable, Optional from trench.exceptions import MFAMethodDoesNotExistError @@ -42,6 +42,14 @@ def get_primary_active_name(self, user_id: Any) -> str: raise MFAMethodDoesNotExistError() return method_name + def fetch_primary_active_name(self, user_id: Any) -> Optional[str]: + method_name = ( + self.filter(user_id=user_id, is_primary=True, is_active=True) + .values_list("name", flat=True) + .first() + ) + return method_name + def is_active_by_name(self, user_id: Any, name: str) -> bool: is_active = ( self.filter(user_id=user_id, name=name) diff --git a/trench/views/base.py b/trench/views/base.py index 53a4dae8..5e21f5ec 100644 --- a/trench/views/base.py +++ b/trench/views/base.py @@ -46,7 +46,6 @@ from trench.settings import SOURCE_FIELD, trench_settings from trench.utils import available_method_choices, get_mfa_model, user_token_generator - User: AbstractUser = get_user_model() @@ -103,6 +102,18 @@ class MFAMethodActivationView(APIView): @staticmethod def post(request: Request, method: str) -> Response: + mfa_model = get_mfa_model() + primary_method = mfa_model.objects.fetch_primary_active_name( + user_id=request.user.id + ) + if primary_method and primary_method != method: + serializer = ChangePrimaryMethodCodeValidator( + user=request.user, + mfa_method_name=primary_method, + data=request.data, + ) + serializer.is_valid(raise_exception=True) + try: source_field = get_mfa_config_by_name_query(name=method).get(SOURCE_FIELD) except MFAMethodDoesNotExistError as cause: