1from threading import local
2
3from django.contrib.auth.backends import ModelBackend
4
5from ..utils import get_user_model
6from . import app_settings
7from .app_settings import AuthenticationMethod
8from .utils import filter_users_by_email, filter_users_by_username
9
10
11_stash = local()
12
13
14class AuthenticationBackend(ModelBackend):
15
16    def authenticate(self, request, **credentials):
17        ret = None
18        if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
19            ret = self._authenticate_by_email(**credentials)
20        elif app_settings.AUTHENTICATION_METHOD \
21                == AuthenticationMethod.USERNAME_EMAIL:
22            ret = self._authenticate_by_email(**credentials)
23            if not ret:
24                ret = self._authenticate_by_username(**credentials)
25        else:
26            ret = self._authenticate_by_username(**credentials)
27        return ret
28
29    def _authenticate_by_username(self, **credentials):
30        username_field = app_settings.USER_MODEL_USERNAME_FIELD
31        username = credentials.get('username')
32        password = credentials.get('password')
33
34        User = get_user_model()
35
36        if not username_field or username is None or password is None:
37            return None
38        try:
39            # Username query is case insensitive
40            user = filter_users_by_username(username).get()
41            if self._check_password(user, password):
42                return user
43        except User.DoesNotExist:
44            return None
45
46    def _authenticate_by_email(self, **credentials):
47        # Even though allauth will pass along `email`, other apps may
48        # not respect this setting. For example, when using
49        # django-tastypie basic authentication, the login is always
50        # passed as `username`.  So let's play nice with other apps
51        # and use username as fallback
52        email = credentials.get('email', credentials.get('username'))
53        if email:
54            for user in filter_users_by_email(email):
55                if self._check_password(user, credentials["password"]):
56                    return user
57        return None
58
59    def _check_password(self, user, password):
60        ret = user.check_password(password)
61        if ret:
62            ret = self.user_can_authenticate(user)
63            if not ret:
64                self._stash_user(user)
65        return ret
66
67    @classmethod
68    def _stash_user(cls, user):
69        """Now, be aware, the following is quite ugly, let me explain:
70
71        Even if the user credentials match, the authentication can fail because
72        Django's default ModelBackend calls user_can_authenticate(), which
73        checks `is_active`. Now, earlier versions of allauth did not do this
74        and simply returned the user as authenticated, even in case of
75        `is_active=False`. For allauth scope, this does not pose a problem, as
76        these users are properly redirected to an account inactive page.
77
78        This does pose a problem when the allauth backend is used in a
79        different context where allauth is not responsible for the login. Then,
80        by not checking on `user_can_authenticate()` users will allow to become
81        authenticated whereas according to Django logic this should not be
82        allowed.
83
84        In order to preserve the allauth behavior while respecting Django's
85        logic, we stash a user for which the password check succeeded but
86        `user_can_authenticate()` failed. In the allauth authentication logic,
87        we can then unstash this user and proceed pointing the user to the
88        account inactive page.
89        """
90        global _stash
91        ret = getattr(_stash, 'user', None)
92        _stash.user = user
93        return ret
94
95    @classmethod
96    def unstash_authenticated_user(cls):
97        return cls._stash_user(None)
98