jazzband / django-two-factor-auth

Complete Two-Factor Authentication for Django providing the easiest integration into most Django projects.
MIT License
1.69k stars 447 forks source link

How to login a user with and without second factor for testing #525

Open hanckmann opened 2 years ago

hanckmann commented 2 years ago

I have seen this issue, but the solution does not (seem to) work for me (anymore): #244

In my application users are onlly required to provide a second factor for the "admin site" and for a number of specific end-points. Therefore, during testing (pytest), I need to login a user with and without providing a second factor.

How do I do this?

My current approach, based on the answer in #244 , works as follows:

from two_factor.utils import default_device
from django_otp import DEVICE_ID_SESSION_KEY
from django_otp.plugins.otp_static.models import StaticDevice

...

    def authenticate_user(self, user=None, mfa=False):

        self.client.logout()
        if user:
            self.client.force_login(user)
            if mfa:
                device = StaticDevice.objects.get_or_create(user=user)[0]
                self.client.session[DEVICE_ID_SESSION_KEY] = device.persistent_id
                self.client.session.save()  # not sure if this is required, but I do it anyway
                print('mfa set')

However, during tests the string "mfa set" is printed, but the endpoints return a 403 error. Manual testing confirms that the issue is in my test (or actually my authentication during the test).

Your Environment

moggers87 commented 2 years ago

Hmm, that's rather odd. What you're doing is basically what we do with Inboxen and that works fine.

I'll look into this more and see if I can reproduce.

moggers87 commented 2 years ago

In fact, it's the same pattern we use on django-two-factor-auth itself: https://github.com/jazzband/django-two-factor-auth/blob/f427f59c03ba091cd1025ad304535e697492b0eb/tests/utils.py#L33-L40

hanckmann commented 2 years ago

Just to be sure I am not doing anything stupid... this is the full method I use for creating users and logging them in.

    def create_default_users(self):
        self.user1 = self.create_user(username="test1", email='test1@voorbeeld.nl')
        self.user2 = self.create_user(username="test2", email='test2@example.com')
        self.user3 = self.create_user(username="test3", email='test3@ejemplo.es')
        self.superuser = self.create_user(
            username="su",
            email="su@test.nl",
            is_staff=True,
            is_superuser=True,
        )

    def create_user(self, username, password=None, email=None, is_staff=False, is_superuser=False, is_active=True):
        if not password:
            password = self.default_password
        if not email:
            email = f'{username}@test.nl'
        return User.objects.create(
            username=username,
            password=password,
            email=email,
            is_staff=is_staff,
            is_superuser=is_superuser,
            is_active=is_active
        )

    def authenticate_user(self, user=None, mfa=False):
        self.client.logout()
        if user:
            self.client.login(username=user.get_username(), password=self.default_password)
            if mfa:
                device = StaticDevice.objects.get_or_create(user=user)[0]
                user.otp_device = device.persistent_id
                user.save()
                self.client.session[DEVICE_ID_SESSION_KEY] = device.persistent_id
                self.client.session.save()

Many of my endpoints are secured using custom permissions. One of the permissions I use (and returns False in tests for the superuser created and logged in as above) is:

class IsAdmin(permissions.BasePermission):
    def has_permission(self, request, view):
        return bool(request.user and request.user.is_staff and request.user.is_verified())

I have checked and noticed that the request.user.is_verified() always returns False.

A lot of my tests are failing, so I do hope I can resolve this and understand where I go wrong... and notice that this only goes wrong during testing. The code works when testing manually.

hanckmann commented 2 years ago

Also, I am not sure if it might be related to: #518

Note that in my case the user does not switch between subdomains. So the cookie should be valid for only that specific subdomain. Hence, I have not changed the SESSION_COOKIE_DOMAIN value (it is not explicitly set in my configs). Therefore I do not think the issues are related, but I want to be sure.