views:

539

answers:

5

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.

+1  A: 

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.

Peter Rowell
See my comment to Daniels answer. Timing attacks are more common than you may realise, this is especially bad if you let the use pick their own password. Also it can be a good idea to tell the user the time of their last login and the number of failed logins when they log in, just in case they notice somthing awry.
gnibbler
And this relates to my answer how? You'll note I didn't tell him what to do, simply how to distinguish between the two cases. In most cases we're not protecting the crown jewels of England, we're trying to reduce the number of brain-dead customer service calls which take time and money to resolve. If you don't know all of the specifics of the problem being solved, including not knowing the political/social context of the specific customer, I have no idea how you could assess the relative value of the solution.
Peter Rowell
Interesting answer as it confirms my suspicion that the only way to do this is to not use the existing form, view and backends that the auth module provides. What you have written above is essentially a new authentication backend which would essentially have to return something other than just the User object or None which, as I state in my question, sort of excludes it from being plugged into the rest of the existing auth code (i.e. the login view, AuthenticationForm and authenticate() method would not be useable). If this is what it takes, fine - it's not a huge amount of code to write.
Steg
You're right, it's not that difficult and it gives you more options based on the demographics of the site. On one site if the username check failed we then checked to see if they had actually typed in their email address instead -- about 25% of them did that, even though there was nothing on the form that said it would work. On another site that has a significantly older demographic (average age is mid-60's and higher) it's a running battle because a surprising number of them can't even remember their email address. Yes, really.
Peter Rowell
+10  A: 

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.

Daniel Roseman
You should take care to make wrong username indistinguishable from wrong password. For example, even if the same message is displayed in 0.1s for wrong username and 0.5s for wrong password a hacker will use that to do a dictionary attack on usernames before going after passwords
gnibbler
Interesting - this is helpful as it sort of describes a "best-practice" rather than a particularly technical solution and it makes me wonder if the django team have developed this module with this in mind. However, it does seem a shame that you have to chip away at the usability of the site just to prevent these tossers from trying to screw you over. I now have to consider how important this is to my site.
Steg
@gnibbler - wowsers, people don't half go to some lengths! I would have never dreamt that people would do such things - what a bunch of freaks! Anyway, out of interest, do you want to post a link or a couple of lines on what you might do to avoid such things - Thanks.
Steg
+1  A: 

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
Jason Christa
I don't think this is actually true. The AuthenticationForm uses the django.contrib.auth.authenticate method (which loops through all available backends) - this returns "None" in the 2 cases where 1) the username is incorrect or 2) the username is correct and the password is incorrect - there distinction is lost here.
Steg
Conceded! Thanks for the tip - +1 for proving me wrong! You've obviously done this before - do you have any comment on the security related answer above from Daniel Roseman?
Steg
Barring the security based answer (on which I haven't made up my mind yet), I think this is certainly the technical solution to the problem. After a little more digging, this essentially describes the approach in the documentation http://docs.djangoproject.com/en/dev/ref/forms/validation/
Steg
If your users find it convenient I would do it. The security vector is pretty small. If you have forums then most user names might be known anyway. If you want to do something meaningful for security, log login attempts and rate limit them and don't let admins use weak passwords.
Jason Christa
A: 

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
Jakobud
Sorry, I should have stated in my question - no stating the bloody obvious:) But seriously, I was looking for some django specific answers, namely if there were any parts of django that I had not come across that would allow me to do the task in hand. Apologies if this was not clear.
Steg
A: 
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.\
                """)
Alex