I've overridden the ModelBackend in my django app. My overridden model backend requires that headers be present in the request to log in the user.
HEADER = 'testing'
class TestingModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
testing_header_value = None
if request is not None and request.META is not None:
testing_header_value = request.META.get(HEADER, None)
if username is None:
username = kwargs.get(User.USERNAME_FIELD)
try:
user = User.objects.get_by_natural_key(username, testing_header_value)
except User.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
User().set_password(password)
else:
# now validate password and whether the user is active
if user.check_password(password) and self.user_can_authenticate(user):
return user
This works perfectly in non test scenarios. However, when I test I'm running into a problem of passing headers with the test client.
The Django test client has a login method but it doesn't pass the request when authenticating which means that my model backend can't function correctly - I can't pass the header I need to. Note that one of the parameters in the authenticate function is the current request.
I see that I can use force_login but that seems a little hack-y. What is the correct way to do this? I suspect subclassing the default test client and overriding the login method might be best but I'm not sure.
I believe force_login() is the best thing to use in your case.
Related
That django authenticate function accepts two parameters, eg
user = authenticate(username=username, password=password)
, but I would like to pass in an additional parameter to validate the entry. Is that possible to do so or can the authenticate function be overridden to achieve that?
Please advise, thanks.
authenticate is just a python function you can use *args & **kwargs to pass multiple params and you can override the authenticate method like (How to override authenticate method ) to add your own validation there.
As far as I know, the authenticate() function takes any keyword arguments as credentials. It simply forwards the credentials to the configured authentication backends. So it really depends on the authentication backend what you do with the passed arguments.
This means you can pass whatever you want to the authenticate() function, but you might need to implement your own authentication backend. You can achieve that by specyfing custom auth backends in the settings.py:
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'confirm.django.auth.backends.EmailBackend',
)
Here's the code for the EmailBackend:
from django.contrib.auth.backends import ModelBackend, UserModel
class EmailBackend(ModelBackend): # pylint: disable=too-few-public-methods
'''
Authentication backend class which authenticates the user against his
e-mail address instead of his username.
'''
def authenticate(self, username=None, password=None, **kwargs): # pylint: disable=unused-argument
'''
Authenticate the user via his e-mail address.
:param str username: The username used to authenticate
:param str password: The password used to authenticate
:return: When successful, the User model is returned
:rtype: User model or None
'''
try:
user = UserModel.objects.get(email=username)
except UserModel.DoesNotExist:
return None
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
return None
When a user is logging in with an e-mail address, the credentials will be passed to the ModelBackend.authenticate() method, which will fail because there's no username matching the e-mail address. After that, the EmailBackend.authenticate() method is called, which will succeed (if the password matches). The first backend returning a User instance "wins".
So it depends what you want to do.
If you simply want to allow users logging in with either their username or e-mail address (or any other field), then this is the way to go. Just create a new auth backend and add it to the AUTHENTICATION_BACKENDS, together with django.contrib.auth.backends.ModelBackend.
If you need an additional field verified for the login, let's say a triplet like customer ID, username & password, you need to create your own backend as well. However, this time the backend should be the only one configured in the AUTHENTICATION_BACKENDS, thus replacing django.contrib.auth.backends.ModelBackend with it.
If you want to know more about customizing authentication, just head over to the brilliant docs.
I am having difficulty understanding how Django's documentation has outlined the overriding of the authenticate method in contrib.auth.models.Users. According to the code below from here, wouldnt the authenticate method succeed if the method was passed a valid username and a valid hash that exists anywhere in the database regardless of whether it matches the password for the supplied primary key field (username, email, etc...) or not. Is there something that check_password is doing that I am not seeing like ensuring that the field that was passed alongside of the password is checked behind the scenes? Because this supplied example appears to have a flaw.
# From Django 1.10 Documentation
def authenticate(self, username=None, password=None):
login_valid = (settings.ADMIN_LOGIN == username)
pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
if login_valid and pwd_valid:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
# Create a new user. There's no need to set a password
# because only the password from settings.py is checked.
user = User(username=username)
user.is_staff = True
user.is_superuser = True
user.save()
return user
return None
Thanks.
authenticate() function returns user for which you attach session using login()
Use authenticate() to verify a set of credentials. It takes
credentials as keyword arguments, username and password for the
default case, checks them against each authentication backend, and
returns a User object if the credentials are valid for a backend. If
the credentials aren’t valid for any backend or if a backend raises
PermissionDenied, it returns None.:
In case of the following authentication backend username and password are passed to it.
Password is compared with one set in Django settings and user object
is queried from database.
If user with that username does not exist backend creates new user.
This backend works even though from security aspect it is not best one :)
In my project, I should create personal type user and company type user they have different attributes. I can create them with different models but I cannot implement both in auth system at the same time? I wonder if it is possible?
As stated in django docs you can write your own AUTHENTICATION_BACKENDS.
For example:
class UserProfileModelBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = self.user_class.objects.get(username=username)
if user.check_password(password):
return user
except self.user_class.DoesNotExist:
return None
def get_user(self, user_id):
try:
return self.user_class.objects.get(pk=user_id)
except self.user_class.DoesNotExist:
return None
#property
def user_class(self):
if not hasattr(self, '_user_class'):
self._user_class = apps.get_model(*settings.AUTH_USER_MODEL.split('.', 2))
if not self._user_class:
raise ImproperlyConfigured('Could not get custom user model')
return self._user_class
And then add this auth-backend to in AUTHENTICATION_BACKENDS in settings.py.
For more information see Writing an authentication backend
When somebody calls django.contrib.auth.authenticate() Django tries authenticating across all of its authentication backends. If the first authentication method fails, Django tries the second one, and so on, until all backends have been attempted.
Be careful:
The order of AUTHENTICATION_BACKENDS matters, so if the same username and password is valid in multiple backends, Django will stop processing at the first positive match.
I'm using Django 1.8.4 on Python 3, and attempting to create an auth backend which validates a cookie from a legacy ColdFusion web site and create / log the Django user in after checking the value in a database. In settings, I am including the backend:
AUTHENTICATION_BACKENDS = (
'site_classroom.cf_auth_backend.ColdFusionBackend',
)
And the code for the backend itself; SiteCFUser is a model against the SQL Server database user model which contains the active cookie token value:
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from users.models import SiteCFUser
class ColdFusionBackend(ModelBackend):
"""
Authenticates and logs in a Django user if they have a valid ColdFusion created cookie.
ColdFusion sets a cookie called "site_web_auth"
Example cookie: authenticated#site+username+domain+8E375588B1AAA9A13BE03E401A02BC46
We verify this cookie in the MS SQL database 'site', table site_users, column user_last_cookie_token
"""
def authenticate(self, request):
User = get_user_model()
print('Hello!')
token=request.COOKIES.get('site_web_auth', None)
print('Token: ' + token)
cookie_bites = token.split('+')
if cookie_bites[0] != "authenticated#site":
# Reality check: not a valid site auth cookie
return None
username = cookie_bites[1]
cf_token = cookie_bites[3]
try:
site_user = SiteCFUser.objects.using('mssqlsite').filter(cf_username=username)
except:
# No user found; redirect to login page
return None
if site_user[0].cftoken == cf_token:
try:
# Does the user exist in Django?
user = User.objects.get(username=username)
except:
# User does not exist, has a valid cookie, create the User.
user = User(username=username)
user.first_name = site_user[0].cf_first_name
user.last_name = site_user[0].cf_last_name
user.email = site_user[0].cf_email
user.save()
else:
return None
def get_user(self, user_id):
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
The problem is, the backend doesn't seem to be called when hitting a URL with a view with #login_required, or even trying to log in through a form with username and password. If I force an error by changing the name of the class in settings, or change the name of the class in cf_auth_backend.py, I do get an error. However, none of the print statements show up in the console. I'm clearly missing something here: any idea what I'm not doing right?
While the accepted answer might have helped the OP, it's not a general answer to the question's title.
Authentication back ends do work simply by listing them in AUTHENTICATION_BACKENDS. But they may appear to be ignored
for various reasons, e.g.:
urls.py needs to point to something like django.contrib.auth.views.login
url(r'^accounts/login/$', django.contrib.auth.views.login)
if it's pointing to some other authentication app. AUTHENTICATION_BACKENDS
may not work.
the authenticate() method must accept a password keyword, either through
password=None or **kwargs. Probably true for username too. It won't
be called if it doesn't accept that keyword argument.
Authentication backends doesn't work that way. They won't be called on each request or on requests where authentication is required.
If you want to log in user based on some cookie, you should call authentication in middleware.
I implemented my own User class from scratch in Django. But when I log in I have this error:
The following fields do not exist in this model or are m2m fields: last_login
I really don't want the field last_login.
I do some reasearch and the problem is here: contrib.aut.models.py
def update_last_login(sender, user, **kwargs):
"""
A signal receiver which updates the last_login date for
the user logging in.
"""
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
user_logged_in.connect(update_last_login)
I found a workaround but it's not an ellegant solution. I added user_logged_in.disconnect(update_last_login) in my models.py file, where my User class is defined.
Is there any better solution for this?
Not sure if this is related to a newer version of django or what, but in my case
user_logged_in.disconnect(update_last_login)
didn't work. This is what works for me (django 2.1):
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
Currently in Django 1.7...
I think the workaround you defined is the only valid solution (besides from a monkey patch) currently when using the Django auth login() method. I'm just going to assume you are using the standard login() method which is raising this exception.
If we take a look at the source for the login method, we find at the end of the method, a call to execute user_logged_in.send(sender=user.__class__, request=request, user=user). We can't prevent this signal from executing besides from disconnecting it as you have pointed out.
Alternatively, we could monkey patch the login() method to remove that signal call.
from django.contrib.auth import login
def monkey_patch_login(request, user):
"""
Persist a user id and a backend in the request. This way a user doesn't
have to reauthenticate on every request. Note that data set during
the anonymous session is retained when the user logs in.
"""
session_auth_hash = ''
if user is None:
user = request.user
if hasattr(user, 'get_session_auth_hash'):
session_auth_hash = user.get_session_auth_hash()
if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or (
session_auth_hash and
request.session.get(HASH_SESSION_KEY) != session_auth_hash):
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
else:
request.session.cycle_key()
request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
request.session[BACKEND_SESSION_KEY] = user.backend
request.session[HASH_SESSION_KEY] = session_auth_hash
if hasattr(request, 'user'):
request.user = user
rotate_token(request)
login = monkey_patch_login
We would put the monkey patch code at the top of the file that needs to call the login() method.