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