def setUp(self): self.someuser = User.objects.get(username='******') # Activate MFA for someuser get_mfa_model().objects.create( user=self.someuser, secret='dummy_mfa_secret', name='app', is_primary=True, is_active=True, _backup_codes='dummy_encoded_codes', ) self.client.login(username='******', password='******')
def setUp(self): self.someuser = User.objects.get(username='******') self.anotheruser = User.objects.get(username='******') # Activate MFA for someuser get_mfa_model().objects.create( user=self.someuser, secret='dummy_mfa_secret', name='app', is_primary=True, is_active=True, _backup_codes='dummy_encoded_codes', ) # Ensure `self.client` is not authenticated self.client.logout()
class Meta: model = get_mfa_model() fields = ( 'name', 'is_primary', 'is_active', 'date_created', 'date_modified', 'date_disabled', )
def test_deactivate_an_only_mfa_method(active_user_with_application_otp): mfa_method = active_user_with_application_otp.mfa_methods.get(name="app") deactivate_mfa_method_command( user_id=active_user_with_application_otp.id, mfa_method_name=mfa_method.name, ) mfa_model = get_mfa_model() mfa_method.refresh_from_db() assert mfa_method.is_active is False assert len( mfa_model.objects.list_active( user_id=active_user_with_application_otp.id)) == 0
def test_date_disabled_is_set_when_not_active(self): mfa_method = get_mfa_model().objects.create( user=self.someuser, secret='dummy_mfa_secret', name='app', is_active=False, _backup_codes='dummy_encoded_codes', ) self.assertNotEqual(mfa_method.date_disabled, None) mfa_method.date_disabled = None mfa_method.save() self.assertNotEqual(mfa_method.date_disabled, None)
def test_remove_not_encrypted_code( active_user_with_non_encrypted_backup_codes): user, codes = active_user_with_non_encrypted_backup_codes settings = TrenchAPISettings(user_settings={"ENCRYPT_BACKUP_CODES": False}, defaults=DEFAULTS) remove_backup_code_command = RemoveBackupCodeCommand( mfa_model=get_mfa_model(), settings=settings).execute code = next(iter(codes)) remove_backup_code_command( user_id=user.id, method_name="email", code=code, )
def test_date_modified(self): mfa_method = get_mfa_model().objects.create( user=self.someuser, secret='dummy_mfa_secret', name='app', is_primary=True, is_active=True, _backup_codes='dummy_encoded_codes', ) date_modified = mfa_method.date_modified mfa_method.save() self.assertNotEqual(date_modified, mfa_method.date_modified) self.assertTrue(date_modified < mfa_method.date_modified)
def test_date_disabled_is_none_when_is_active(self): mfa_method = get_mfa_model().objects.create( user=self.someuser, secret='dummy_mfa_secret', name='app', is_primary=True, is_active=True, _backup_codes='dummy_encoded_codes', ) self.assertEqual(mfa_method.date_disabled, None) mfa_method.date_disabled = now() mfa_method.save() self.assertEqual(mfa_method.date_disabled, None)
def post(request: Request) -> Response: serializer = MFAMethodCodeSerializer(data=request.data) serializer.is_valid(raise_exception=True) try: method = serializer.validated_data.get("method") mfa_model = get_mfa_model() if method is None: method = mfa_model.objects.get_primary_active_name( user_id=request.user.id) mfa = mfa_model.objects.get_by_name(user_id=request.user.id, name=method) return get_mfa_handler(mfa_method=mfa).dispatch_message() except MFAValidationError as cause: return ErrorResponse(error=cause)
def clean(self): cleaned_data = super().clean() # `super().clean()` initialize the object `self.user_cache` with # the user object retrieved from authentication (if any) auth_method = get_mfa_model().objects.filter( is_active=True, user=self.user_cache).first() # Because we only support one 2FA method, we do not filter on # `is_primary` too (as django_trench does). # ToDo Figure out why `is_primary` is False sometimes after reactivating # 2FA if auth_method: self.ephemeral_token_cache = (user_token_generator.make_token( self.user_cache)) return cleaned_data
def validate_mfa_not_active(self, user: '******'): """ Raise an exception if MFA is enabled for user's account. """ # This condition is kind of temporary. We can activate/deactivate # class based on settings. Useful until we decide whether # TokenAuthentication should be deactivated with MFA # ToDo Remove the condition when kobotoolbox/kpi#3383 is released/merged class_path = f'{self.__module__}.{self.__class__.__name__}' if class_path not in settings.MFA_SUPPORTED_AUTH_CLASSES: if get_mfa_model().objects.filter(is_active=True, user=user).exists(): raise exceptions.AuthenticationFailed(gettext( 'Multi-factor authentication is enabled for this account. ' f'{self.verbose_name} cannot be used.' ))
def post(request: Request) -> Response: mfa_model = get_mfa_model() mfa_method_name = mfa_model.objects.get_primary_active_name( user_id=request.user.id) serializer = ChangePrimaryMethodValidator( user=request.user, mfa_method_name=mfa_method_name, data=request.data) serializer.is_valid(raise_exception=True) try: set_primary_mfa_method_command( user_id=request.user.id, name=serializer.validated_data["method"]) return Response(status=HTTP_204_NO_CONTENT) except MFAValidationError as cause: return ErrorResponse(error=cause)
def post(self, request: Request) -> Response: serializer = LoginSerializer(data=request.data) serializer.is_valid(raise_exception=True) try: user = authenticate_user_command( request=request, username=serializer.validated_data[User.USERNAME_FIELD], password=serializer.validated_data["password"], ) except MFAValidationError as cause: return ErrorResponse(error=cause) try: mfa_model = get_mfa_model() mfa_method = mfa_model.objects.get_primary_active(user_id=user.id) get_mfa_handler(mfa_method=mfa_method).dispatch_message() return Response( data={ "ephemeral_token": user_token_generator.make_token(user), "method": mfa_method.name, }) except MFAMethodDoesNotExistError: return self._successful_authentication_response(user=user)
def validate_code(self, value: str) -> str: if not value: raise OTPCodeMissingError() mfa_model = get_mfa_model() mfa = mfa_model.objects.get_by_name(user_id=self._user.id, name=self._mfa_method_name) self._validate_mfa_method(mfa) validated_backup_code = validate_backup_code_command( value=value, backup_codes=mfa.backup_codes) handler = get_mfa_handler(mfa) validation_method = getattr(handler, self._get_validation_method_name()) if validation_method(value): return value if validated_backup_code: remove_backup_code_command(user_id=mfa.user_id, method_name=mfa.name, code=value) return value raise CodeInvalidOrExpiredError()
from django.db.transaction import atomic from typing import Type from trench.exceptions import MFAMethodDoesNotExistError, MFAPrimaryMethodInactiveError from trench.models import MFAMethod from trench.utils import get_mfa_model class SetPrimaryMFAMethodCommand: def __init__(self, mfa_model: Type[MFAMethod]) -> None: self._mfa_model = mfa_model @atomic def execute(self, user_id: int, name: str) -> None: self._mfa_model.objects.filter(user_id=user_id, is_primary=True).update( is_primary=False ) if not self._mfa_model.objects.is_active_by_name(user_id=user_id, name=name): raise MFAPrimaryMethodInactiveError() rows_affected = self._mfa_model.objects.filter( user_id=user_id, name=name ).update(is_primary=True) if rows_affected < 1: raise MFAMethodDoesNotExistError() set_primary_mfa_method_command = SetPrimaryMFAMethodCommand( mfa_model=get_mfa_model() ).execute
from trench.settings import api_settings from trench.utils import ( create_secret, get_mfa_handler, get_mfa_model, get_nested_attr, set_nested_attr, user_token_generator, validate_backup_code, validate_code, ) User = get_user_model() MFAMethod = get_mfa_model() mfa_methods_items = api_settings.MFA_METHODS.items() MFA_METHODS = [ (k, v.get('VERBOSE_NAME', _(k))) for k, v in mfa_methods_items ] class RequestMFAMethodActivationSerializer(serializers.Serializer): serializer_field_mapping = { django_models.AutoField: fields.IntegerField, django_models.BigIntegerField: fields.IntegerField, django_models.BooleanField: fields.BooleanField, django_models.CharField: fields.CharField, django_models.CommaSeparatedIntegerField: fields.CharField, django_models.DateField: fields.DateField,
from django.db.transaction import atomic from typing import Type from trench.exceptions import DeactivationOfPrimaryMFAMethodError, MFANotEnabledError from trench.models import MFAMethod from trench.utils import get_mfa_model class DeactivateMFAMethodCommand: def __init__(self, mfa_model: Type[MFAMethod]) -> None: self._mfa_model = mfa_model @atomic def execute(self, mfa_method_name: str, user_id: int) -> None: mfa = self._mfa_model.objects.get_by_name(user_id=user_id, name=mfa_method_name) number_of_active_mfa_methods = self._mfa_model.objects.filter( user_id=user_id, is_active=True).count() if mfa.is_primary and number_of_active_mfa_methods > 1: raise DeactivationOfPrimaryMFAMethodError() if not mfa.is_active: raise MFANotEnabledError() self._mfa_model.objects.filter( user_id=user_id, name=mfa_method_name).update(is_active=False) deactivate_mfa_method_command = DeactivateMFAMethodCommand( mfa_model=get_mfa_model()).execute
class Meta: model = get_mfa_model() fields = ("name", "is_primary")
from typing import Callable, Type from trench.command.create_secret import create_secret_command from trench.exceptions import MFAMethodAlreadyActiveError from trench.models import MFAMethod from trench.utils import get_mfa_model class CreateMFAMethodCommand: def __init__(self, secret_generator: Callable, mfa_model: Type[MFAMethod]) -> None: self._mfa_model = mfa_model self._create_secret = secret_generator def execute(self, user_id: int, name: str) -> MFAMethod: mfa, created = self._mfa_model.objects.get_or_create( user_id=user_id, name=name, defaults={ "secret": self._create_secret, "is_active": False, }, ) if not created and mfa.is_active: raise MFAMethodAlreadyActiveError() return mfa create_mfa_method_command = CreateMFAMethodCommand( secret_generator=create_secret_command, mfa_model=get_mfa_model() ).execute
def get_queryset(self) -> QuerySet: mfa_model = get_mfa_model() return mfa_model.objects.list_active(user_id=self.request.user.id)