Maybe a dumb question, but if possible, what's the best way of writing a BasePasswordHasher subclass using username as a part of the salt? I'm rewriting a site from scratch and used this approach in php. The problem is accessing the username in a password hasher. I would be really happy to resolve this as a LOT of users would lose their passwords otherwise, so a big thanks in advance!
PHP Code:
function passHash($login, $pass)
{
return md5(md5($pass).'salt'.$login);
}
As you noticed, this cannot be done in the password hasher alone. The password hasher does not have information about the user, only the password and hash. I think you have two options.
First, and probably best, is to write a custom authentication backend. At the authentication backend level, we have access to the username and raw password. It would look like this
# settings.py
AUTHENTICATION_BACKENDS=(
'myapp.backends.LegacyBackend',
'django.contrib.auth.backends.ModelBackend',
)
# myapp.backends
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.utils.encoding import force_bytes
import hashlib
class LegacyBackend(ModelBackend):
# We only need to override the authenticate method
def authenticate(self, username=None, password=None, **kwargs):
# most of this is copied directly from ModelBackend's authenticate method
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
# This is the normal route that hands off to the password hasher pipeline
# but we will sidestep it entirely and verify the password here
#
# if user.check_password(password):
# return user
pwhash = hashlib.md5(force_bytes(password)).hexdigest()
hash = hashlib.md5(force_bytes(pwhash+"salt"+username)).hexdigest()
if hash == user.password:
# update the user's password if you want, so you can phase out this backend
user.set_password(password)
user.save(update_fields=["password"])
return user
except UserModel.DoesNotExist:
UserModel().set_password(password)
Note that I haven't tested this code, but it should work as advertised. In addition, you don't have conflicts with new users, and old users will have their passwords updated to the new hashing algorithm (default is PBKDF2+SHA256? not sure).
The second option is to write a one-off script to modify your database so the user.password fields look like legacymd5$username+salt$hash. Then you can write your custom password hasher as planned.
For anyone who finds this post as I did, everything still works as expected, except one thing. On Django 2.1, I found I had to add "request" as the first argument in the authenticate method. They must have added passing this thru at some point. I was silently failing the auth and had no idea why.
Related
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 :)
I am kind of new to django and I am concerned about can we use multiple models for authentication for different types of users for our application e.g for Customers use Customer model, for Suppliers use Supplier model and also keep default User registration model for administration use only? If so can you point me in the right direction, how it can be done? Which package should be used?
There is one way that I came around is by adding a foreign key to each model viz needed for authentication but that will involve joins in every query which could result in performance issues. I need to know if there is a better way. An also these custom models can benefit for all permissions stuff available in admin panel.
Expert opinion will be really appreciated.
There are a few different options to handle that.
Maybe check first the Django-docu.
I've you'ld like to customize it to authenticate your users with a mail-address, here is an example written by knbk and rahul-gupta on the ex-documentation:
Django's default authentication works on username and password fields. Email authentication backend will authenticate users based on email and password.
from django.contrib.auth import get_user_model
class EmailBackend(object):
"""
Custom Email Backend to perform authentication via email
"""
def authenticate(self, username=None, password=None):
user_model = get_user_model()
try:
user = user_model.objects.get(email=username)
if user.check_password(password): # check valid password
return user # return user to be authenticated
except user_model.DoesNotExist: # no matching user exists
return None
def get_user(self, user_id):
user_model = get_user_model()
try:
return user_model.objects.get(pk=user_id)
except user_model.DoesNotExist:
return None
Add this authentication backend to the AUTHENTICATION_BACKENDS setting.
# settings.py
AUTHENTICATION_BACKENDS = (
'my_app.backends.EmailBackend',
...
)
I have used Django before (version 1.2) and generally I like it... it is especially good at getting a brand new project up and running quickly. But, in this case, I'm rewriting and existing system and moving it to Python/Django. So, I already have a MySQL database that has a "users" table in it... this table stores the user's password with the MySQL SHA1 function (no salt, etc).
As part of the migration, I'm going to fix some of the data modeling flaws and port to PostgreSQL.
I would really like to use django.contrib.auth, but I'm unclear what I need to do. I have read the documentation, and know that I can separate the required user information and the "extra" information I have and put it into UserProfile.
But, how to handle the passwords stored in the MySQL db?
Has anyone handled this before? What approach did you take?
Here is what I did to get things working. I created a custom authentication backend. Note: I'm using the email address as the username.
Here is my code:
from django.db.models import get_model
from django.contrib.auth.models import User
from hashlib import sha1
class MyUserAuthBackend(object):
def check_legacy_password(self, db_password, supplied_password):
return constant_time_compare(sha1(supplied_password).hexdigest(), db_password)
def authenticate(self, username=None, password=None):
""" Authenticate a user based on email address as the user name. """
try:
user = User.objects.get(email=username)
if '$' not in user.password:
if self.check_legacy_password(user.password, password):
user.set_password(password)
user.save()
return user
else:
return None
else:
if user.check_password(password):
return user
except User.DoesNotExist:
return None
def get_user(self, user_id):
""" Get a User object from the user_id. """
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Then I added the following to settings.py:
AUTHENTICATION_BACKENDS = (
'my_website.my_app.my_file.MyUserAuthBackend',
)
The suggestion from #Dougal appears to be for the next release of Django and was not available for me (I'm using 1.3.1). However, it seems like it will be a better solution.
You can probably put it straight into the user_password field - see the Django docs. Since you don't have a salt, try using the format sha1$$password_hash. I haven't investigated to see that it'll work with a blank salt, but that's probably the only way you're going to be able to migrate it without hacking django.contrib.auth or writing your own authentication backend.
Otherwise, you could just set an unusable password (the canonical thing to do is set the field to !) for users and point them to the forgot-password system.
Recent versions of Django provide a hasher for unsalted legacy passwords. Just add this to your settings file:
PASSWORD_HASHERS = (
...
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
)
i want to authenticate users using firstname and lastname
This is the code i am using
user = auth.authenticate(first_name=firstname,last_name=lastname,password=password)
it keep coming up with NoneType: None
i have checked the firstname and lastname plus password seen to be correct?
what i am doing wrong? thanks
The difficulty here is that normally you'd handle this by creating a custom authentication backend that implements authenticate and get_user. However, the function signature for authenticate is:
def authenticate(self, username=None, password=None):
Everywhere in Django that would be calling this will be passing only 2 parameters, username and password. This means that using any of the generic authentication forms and things like the admin interface will break if this is done any other way.
The only work around I could see, and this is kind of sketchy, is if the username were to be typed as a single entry with a string "First Last" (delimited by a space) in place of the username. You could then separate it out and use that value...
(this is all untested, but you get the idea)
class FirstLastNameBackend(object):
def authenticate(self, username=None, password=None):
first, last = username.split(' ', 1)
try:
user = User.objects.get(first_name=first, last_name=last)
if user:
# Check if the password is correct
# check if the user is active
# etc., etc.
return user
except:
pass
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except:
return None
The django doc provides a lot of helpful details on doing a custom backend: User auth with custom backend
On a side note, something to be careful of is last names that have a space(s) in them, like "de la Cruz". If you specify 1 for maxsplit on the split function, you'll avoid this problem.
Building a little from #T. Stone's idea. Why not have them register with their First and Last name and you just concatenate them together and use that as their username?. And everytime you have them login you setup your view to combine the two fields again and use that string.
You won't be able to use some of the auto forms they can produce for you but that's not a big deal. I'd just combine the two strings, lowercase them and slap that as the username and do the same for every login instance.
You can use any parameters in backend authentication function, i.e.:
class FirstLastNameBackend(object):
def authenticate(self, first_name=None, last_name=None, password=None):
pass #your user auth code goes here
In order to authenticate user you call
user = auth.authenticate(first_name=firstname,
last_name=lastname,
password=password)
One drawback, however, that you'll need to implement your own log in form and this authentication won't be supported in admin interface.
I like the idea of not making users remember a username, but I think a better solution to that is to have their email address be their user name. Is it fair for you to assume in your specific application that no two users will have the same first and last name? If that's not a fair assumption, how will your system handle that?
When using the standard authentication module in django, a failed user authentication is ambiguous. Namely, there seems to be no way of distinguishing between the following 2 scenarios:
Username was valid, password was invalid
Username was invalid
I am thinking that I would like to display the appropriate messages to the user in these 2 cases, rather than a single "username or password was invalid...".
Anyone have any experience with simple ways to do this. The crux of the matter seems to go right to the lowest level - in the django.contrib.auth.backends.ModelBackend class. The authenticate() method of this class, which takes the username and password as arguments, simply returns the User object, if authentication was successful, or None, if authentication failed. Given that this code is at the lowest level (well, lowest level that is above the database code), bypassing it seems like a lot of code is being thrown away.
Is the best way simply to implement a new authentication backend and add it to the AUTHENTICATION_BACKENDS setting? A backend could be implemented that returns a (User, Bool) tuple, where the User object is only None if the username did not exist and the Bool is only True if the password was correct. This, however, would break the contract that the backend has with the django.contrib.auth.authenticate() method (which is documented to return the User object on successful authentication and None otherwise).
Maybe, this is all a worry over nothing? Regardless of whether the username or password was incorrect, the user is probably going to have to head on over to the "Lost password" page anyway, so maybe this is all academic. I just can't help feeling, though...
EDIT:
A comment regarding the answer that I have selected:
The answer I have selected is the way to implement this feature. There is another answer, below, that discusses the potential security implications of doing this, which I also considered as the nominated answer. However, the answer I have nominated explains how this feature could be implemented. The security based answer discusses whether one should implement this feature which is, really, a different question.
You really don't want to distinguish between these two cases. Otherwise, you are giving a potential hacker a clue as to whether or not a username is valid - a significant help towards gaining a fraudulent login.
This is not a function of the backend simply the authentication form. Just rewrite the form to display the errors you want for each field. Write a login view that use your new form and make that the default login url. (Actually I just saw in a recent commit of Django you can now pass a custom form to the login view, so this is even easier to accomplish). This should take about 5 minutes of effort. Everything you need is in django.contrib.auth.
To clarify here is the current form:
class AuthenticationForm(forms.Form):
"""
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""
username = forms.CharField(label=_("Username"), max_length=30)
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
def __init__(self, request=None, *args, **kwargs):
"""
If request is passed in, the form will validate that cookies are
enabled. Note that the request (a HttpRequest object) must have set a
cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before
running this validation.
"""
self.request = request
self.user_cache = None
super(AuthenticationForm, self).__init__(*args, **kwargs)
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username and password:
self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
raise forms.ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
elif not self.user_cache.is_active:
raise forms.ValidationError(_("This account is inactive."))
# TODO: determine whether this should move to its own method.
if self.request:
if not self.request.session.test_cookie_worked():
raise forms.ValidationError(_("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in."))
return self.cleaned_data
def get_user_id(self):
if self.user_cache:
return self.user_cache.id
return None
def get_user(self):
return self.user_cache
Add:
def clean_username(self):
username = self.cleaned_data['username']
try:
User.objects.get(username=username)
except User.DoesNotExist:
raise forms.ValidationError("The username you have entered does not exist.")
return username
We had to deal with this on a site that used an external membership subscription service. Basically you do
from django.contrib.auth.models import User
try:
user = User.objects.get(username=whatever)
# if you get here the username exists and you can do a normal authentication
except:
pass # no such username
In our case, if the username didn't exist, then we had to go check an HTPASSWD file that was updated by a Perl script from the external site. If the name existed in the file then we would create the user, set the password, and then do the auth.
This answer is not specific to Django, but this is the pseudo-code I would use to accomplish this:
//Query if user exists who's username=<username> and password=<password>
//If true
//successful login!
//If false
//Query if user exists who's username=<username>
//If true
//This means the user typed in the wrong password
//If false
//This means the user typed in the wrong username
def clean_username(self):
"""
Verifies that the username is available.
"""
username = self.cleaned_data["username"]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return username
else:
raise forms.ValidationError(u"""\
This username is already registered,
please choose another one.\
""")