Django username field nullable breaks default auth backend - django

when using a custom User model, extending AbstractBaseUser, I've run into a problem when the USERNAME_FIELD points to a nullable field
what I'm trying to accomplish is to allow multiple methods of login (facebook, username) and for that I have two unique identifier fields (facebook_id, username) but both have to be nullable because an account doesn't have to have both (and most accounts won't have both)
so for logging in with username, I rely on Django's built in login auth backend
where it falls apart is when trying to log in with an alternative login method (facebook) and the Django auth is attempted first
class ModelBackend(object):
"""
Authenticates against settings.AUTH_USER_MODEL.
"""
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
if user.check_password(password):
return user
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
the if username is None hack is meant to fix the situation where the username field isn't actually called "username" so the username parameter is empty but the actual provided username can be found in kwargs
the issue is that it ends up with None value anyway (because kwargs contains facebook_id=12345) and ends up passing that to get_by_natural_key
def get_by_natural_key(self, username):
return self.get(**{self.model.USERNAME_FIELD: username})
which translates roughly to User.objects.get(username=None)
since my username field is nullable, this returns multiple results, the whole thing throws an models.MultipleObjectsReturned exception ("get() returned more than one User -- it returned 417!") and prevents any other auth backends from running
from my perspective there's several issues:
ModelBackend shouldn't be just blindly attempting to retrieve a user if its attempts at figuring out the provided username result with None
get_by_natural_key should catch the MultipleObjectsReturned and gracefully fail, or authenticate() should handle that scenario and fail to log the user in, instead of bombing the entire auth mechanism
known workarounds:
I know I can move the ModelBackend to be the last method attempted but that just makes the dumb query run sometimes instead of always, I'd rather fix it so it never does. I'm also slightly concerned with its performance as number of username=None users grows
I also know I can extend ModelBackend, patch the holes and just not use the stock ModelBackend, but I don't know if there are any gotchas to that approach... are existing sessions using ModelBackend lost?
For simplification purposes, I skipped the part where I actually use a nullable email field as the username :D and I know that for that I could just easily use a custom auth backend but once I got nullable-email-field-login working I started thinking I can probably save myself some headache in generating usernames for users coming from facebook (and letting them choose an unused username later on in the process) by making username field nullable as well :D
I'm concerned that having a nullable USERNAME_FIELD is going to make Django fail in various different ways, and I saw no warning for this or explanation for why. It's silly that a system that supports multiple login schemes cannot survive it's stock auth identifier being empty.

Since you're replacing the default User model with your own, I'd go with patching its manager's get_by_natural_key method to raise DoesNotExist when called with a None natural key, as that situation clearly indicates your user is being authenticated using some other scheme. Your model has no situation where a record can have a legitimate None natural key, so in any case it should raise a DoesNotExist for such a key.

Related

Longer username in Django 1.7

I want to increase the length of the username in django from 30 to around 80, I know it may be duplicate question but the previous answers are not working, for example https://kfalck.net/2010/12/30/longer-usernames-for-django
this is for Django 1.2.
Did anyone try similar hack for Django>1.5
Thanks in advance
In Django 1.5 and above, the recommended approach would be to create a custom user model. Then you can make the username field exactly as you want.
I had the same problem few days ago. Finally, I ended just with cutting off first 30 characters of the (old) username (into the new database table), and adding a custom authentication backend that will check the email instead of user name. Terrible hack I know, and I'm planning to fix it as soon as I have some time. The idea is following:
I already have a model class that has one-to-one relation with djangos auth.User. I will add another field there called full_username.
class MyCustomUserModel(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, related_name="custom_user")
full_username = models.CharField(max_length=80, ...)
...
Then, I'll add another custom authentication backend that will check this field as username. It would look something like this:
from django.contrib.auth.backends import ModelBackend
class FullUsernameAuthBackend(ModelBackend):
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.filter(custom_user__full_username=username)
# If this doesn't work, will use (the second case):
# user = MyCustomUserModel.objects.filter(full_username=username).user
if user.check_password(password):
return user
except UserModel.DoesNotExist:
# Adding exception MyCustomUserModel.DoesNotExist in "(the second case)"
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
After this, you need to change settings.py:
AUTHENTICATION_BACKENDS = (
"....FullUsernameAuthBackend",
# I will have the email auth backend here also.
)
I hope that it will work.
Custom User Models are a huge change to make and aren't always compatible with apps. I solved it by running this very pragmatic migration. Note this only solves it at the database level.
migrations.RunSQL("alter table auth_user alter column username type varchar(254);")

Django Social Auth store username from Facebook in Custom User Model

I've just integrated Django-Social-Auth (DSA) v0.7.23 using django 1.5 with a custom User model and auth/login is working fine except for the username field which is not being stored in my custom User model.
Stepping through the DSA code it appears to explicitly remove the username which was passed back by facebook. Here is the function in question that pops the username off:
#classmethod
def username_field(cls, values):
user_model = cls.user_model()
if hasattr(user_model, 'USERNAME_FIELD'):
# Django 1.5 custom user model, 'username' is just for internal
# use, doesn't imply that the model should have an username field
values[user_model.USERNAME_FIELD] = values.pop('username') # Username removed
return values
How can I go about getting the username that was passed back from facebook and not having it be explicitly removed by DSA?
I believe a work around would be to create a custom pipeline that generates a username. However I was wondering if anyone else encountered this scenario before and leveraged anything that already exists within DSA (i.e. a particular settings.py configuration)
Thanks.
The original username is available in the details attribute passed to pipeline functions, take this for example:
def generated_username(user, details, *args, **kwargs):
username = details['username']
user.your_field = username
user.save()
It's worth noting that the username is popped from values if you have a USERNAME_FIELD defined in your custom model.

Separate admin site for users

I'm kinda new to Django and looking for some advice on a medium sized project. Our in-office admin is one thing, but our users can also log in online and see a similar (more limited, but mostly the same) interface.
The problem is that the users email is the username, and emails don't have to be unique (but passwords must for reasons you understand). Ghetto? Yes. But it's what they expect.
Is it possible to create this second interface as well as deviate away from the traditional username for sign in? Does Django 1.5s custom user model do anything for me here? And is it possible to make use of two user models--one for the admin, and one for our users?
And finally, if I'm heading in the wrong direction entirely, please let me know. This is a follow-up to a question I'd asked previously, but I'd forgotten about some of the details.
EDIT: Much as I hate to do it, I'll just code the member area seperately. More weirdness with the logins has emerged so I think this will be the easiest solution, even if not very DRY.
Having two admin interfaces is probably silly. Django's admin supports robust user permissions.
Keep username's unique, and use an email authentication backend. This lets users user their email address if they want to. Since, at least in 1.4, email addresses didn't have to be unique, you're in the clear.
Here's a simple email auth backend:
from django.contrib.auth.backends import ModelBackend
from django.contrib.admin.models import User
class EmailAuthBackEnd(ModelBackend):
def authenticate(self, email=None, password=None,**kwargs):
try:
user = User.objects.get(email=email)
if user.check_password(password):
return user
return None
except User.DoesNotExist:
return None
Add the above to a file called backends.py in one of your apps, then add this to your settings.py:
AUTHENTICATION_BACKENDS = AUTHENTICATION_BACKENDS + ('app.backends.EmailAuthBackEnd',)
On the whole unique passwords thing … that's tricky to deal with. You could subclass Django's user model, and change password to a unique string, or you could patch PSQL manually to enforce that constraint. OR you could be lazy, assume passwords will be assigned, assign every user a random username, safe in the knowledge you can always login and 'fix' problem accounts.
I would suggest that you allow users to retain their current credentials (that will be a big plus from a usability point of view), and then adjust what they see depending on where they're coming from. There's lots of methods on django's ModelAdmin that you can override. So on a model by model basis you could do -
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
self.exclude = []
if request.META(REMOTE_ADDR) not in [# add list of internal ip's here]
self.exclude.append('field_to_hide')
return super(MyModelAdmin, self).get_form(request, obj, **kwargs)
As in the docs example here. (also have a look at the list of http headers here to get an idea of how you might make your check)
If you want to change the list of objects available on the main admin menu then, look at overriding the AdminSite, to add your own get_urls() method that provides similar functionality to the individual get_form method I've suggested above.

Anyone think django's user model is too tightly coupled with auth?

I'm trying to learn Django and I would like feedback from anyone who has any MVC/MTV/PHP/Ruby framework experience. Does anyone find that the user model is too tightly coupled with auth?
Background: When you first implement authentication for Django, you include the module django.contrib.auth
This will bring in several models like User, Group, Message etc. Let's focus on the User model as this is the one of the most important tables in any website.
In short the User table has these fields
User
username max_length 30, unique, [letters, digits, underscores]
password max_length 75
email max_length 75
...and about 8 other useful fields like first_name, last_name, etc.
Goal:
I want to remove username and use email as the login for every user. It's a pretty simple request that many websites use these days.
I don't want to monkey patch the core code since this will make upgrading more difficult later on. This means modifying the User model is out of the question. I only want to do a few simple and basic things I expect a few frameworks to do so let me address how Django does it.
Adding new fields to the User model
Django docs says to use create another table and insert the fields there. You will have a one to one relationship between the User table and the Profile table.
eg.
If You want to add an image field to each user you add it to the profile table. A join query is made every single time. They've even specified a constant to tell the framework what table to use:
AUTH_PROFILE_MODULE = 'accounts.UserProfile'
I don't think it's the best practice to have to do a join query every time I want a field that should belong to the user table.
Another option is to use the function add_to_class.
The django community has stated it's not good to define new fields outside of the main class because other developers who add methods won't know all the data members.
Editing old fields
The auth module does a check against two fields username and the hashed password. Looking at the above table I would need to change the username model to accept these properties. Length of 75 with all the valid characters of the email. The django suggests I check against the email field.
Two problems arise if I use the email field to auth against:
I need to write a new class to be used in a constant AUTHENTICATION_BACKEND, so it checks against the email field and I have an unused field called username.
Adding new methods
In MVC/MTV a design principle is to use fat models skinny controllers. Since the model is declared in auth, I'm not sure how one is supposed to add methods that act on the user model's fields. Since django suggests using a Profile model, I suppose they will have to go there.
Extending the User class
A small annoyance would be that I can't use the name 'User' and instead must use 'Users' or 'Accounts'. A bigger one is I don't think the auth would recognize this new module. Meaning I would have to rewrite a bunch functionality that is is present. This one doesn't bother me as it's something I expect to do in other frameworks.
Any comments are appreciated. I wouldn't ask all these questions and look for solutions if I wasn't truly interested in using django.
I agree that django's incessant clinginess to the auth models is absurd. My job requires me to create ultra scalable and very high load sites which sometimes require user authentication and djano's auth model + permissions does not fit with that.
Fortunately, it's not difficult to replace.
First, create a custom User model.
class User(models.Model):
...fields...
#Define some interface methods to be compatible.
def get_and_delete_messages(self):
def is_active(self):
def is_anonymous(self):
def is_authenticated(self):
def is_staff(self):
def has_perm(self, perm_list):
Second, create your own authentication back-end.
class LocalAccount(object):
"""
This checks our local user DB for authentication
"""
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(alias=username)
if user.check_password(password):
return user
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.select_related().get(pk=user_id)
except User.DoesNotExist:
return None
#settings.py
AUTHENTICATION_BACKENDS = (
'helpers.auth.LocalAccount',
)
That should solve most of your issues, I don't even think all of the methods you would find on django.contrib.auth.User are necessary, I recommend trying it out.
The one gotcha here is that the admin may start to bitch, fortunately that's really easy to patch using simple python inheritance as well. That's another question though :)
At the end of the day your project's auth backend needs some sort of store for auth credentials. That the default auth backend is tightly coupled to the User model is not strange in this respect. It's easy enough to substitute your own definition for the user model if you write your own auth backend, as I have in the past.
I created my Profile model and use AUTH_PROFILE_MODULE, so I have complete control over my model, I can modify fields, add methods, etc. Now I'm thinking about using cache and writing middleware that will get profile from cache if possible.
To login using email you could write very simple auth backend:
from django.contrib.auth.models import User
from django.contrib.auth.backends import ModelBackend
class EmailModelBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(password):
return user
except User.DoesNotExist:
return None

Django: how to store subdomain-based authentication usernames?

I need to create a subdomain based authentication system, like the one 37signals, freshbooks, codebase use. That is, each subdomain of my main application needs to have its own username namespace. I would like to keep as much as possible of the django authentication system.
What is a good way to store the username?
In particular, it should be possible for different users to have the same username as long as their account belongs to a different subdomain.
Some approaches I've considered, for which I can foresee shortcomings:
storing some prefix in the username field of the django auth user model.
extending the user model according to this.
customizing the source of auth to my needs
I have built this functionality for several sites in the past and have found that your first bullet point is the way to go.
It means that you don't have to make massive change to django auth. What I did was set up a custom authentication backend that abstracts away the way usernames are stored.
auth_backends.py
from django.contrib.auth.backends import ModelBackend
from Home.models import Account
class CustomUserModelBackend(ModelBackend):
def authenticate(self, subdomain, email, password):
try:
user = Account.objects.get(username=u'%s.%s' % (subdomain, email))
if user.check_password(password):
return user
except Account.DoesNotExist:
return None
def get_user(self, user_id):
try:
return Account.objects.get(pk=user_id)
except Account.DoesNotExist:
return None
For this particular project Account was the user model and it just inherited directly from User however you could replace Account with whatever you want.
You have to install the custom auth backend in your settings file:
AUTHENTICATION_BACKENDS = (
'auth_backends.CustomUserModelBackend',
'django.contrib.auth.backends.ModelBackend',
)
Then when you call authenticate you need to pass in the subdomain, email and password.
You can also add some other helper functions or model methods that help with making sure that only the user's actual username is displayed, but that is all pretty trivial.
I think this may be a good use case for using django.contrib.sites in combination with the second bullet item you mentioned. You could create a CustomUser model like so:
from django.contrib.sites.models import Site
class CustomUser(User):
"""User with app settings."""
sites = models.ManyToManyField(Site)
Then you could write a custom auth backend to check that the user can sign in to the current subdomain using the supplied credentials. This allows you to have one username for multiple sites (subdomains) without having to hack the internal auth app or store multiple usernames with custom prefixes.
EDIT: you can get the current site by using Site.objects.get_current() and then check to see if the current site is in the user's sites.
You can read more about the sites framework here: http://docs.djangoproject.com/en/dev/ref/contrib/sites/