+2  A: 

I've done similar things by writing my own authentication backend and putting it in the authenticate() method. The code is public and up here. I also included a pluggable system of "mappers" to do most of the work that isn't just authenticating the user (eg, getting fullname from ldap, automatically creating groups based on "affiliations" that our auth service gives us, and mapping certain users and affiliations into staff/superuser roles automatically).

Basically, the authenticate method looks like:

def authenticate(self, ticket=None):
    if ticket is None:
        return None
    # "wind" is our local auth service
    (response,username,groups) = validate_wind_ticket(ticket)
    if response is True:
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            user = User(username=username, password='wind user')
            user.set_unusable_password()
            # give plugins a chance to pull up more info on the user
            for handler in self.get_profile_handlers():
                handler.process(user)
            user.save()
        # give plugins a chance to map affiliations to groups
        for handler in self.get_mappers():
            handler.map(user,groups)
        return user
    else:
        # i don't know how to actually get this error message
        # to bubble back up to the user. must dig into
        # django auth deeper. 
        pass
    return None

So I pretty much agree with you that authentication should be just a yes/no affair and other stuff should happen elsewhere, but I think with the way Django sets things up, the path of least resistance is to put it in with authentication. I do recommend making your own authentication code delegate that stuff to plugins though since that's within your control.

I'm only fetching the LDAP data on their very first login though (when the auth_user row gets added). Anytime they login after that, it just uses what it already has locally. That means that if their LDAP info changes, it won't automatically propagate down to my apps. That's a tradeoff I'm willing to make for simplicity.

I'm not sure why you're running into problems with the first login though; I'm taking a very similar approach and haven't run into that. Maybe because the login process on my apps always involves redirecting them to another page immediately after authentication, so the dummy request.user never gets touched?

thraxil
Would you mind if I asked what version of Django are you running? You should see the same problem I do - you are doing a `user.save()` in the `authenticate` method just like me and the `django.contrib.auth.login()` call should try to save based on the `request.user` and fail because of a unique constraint violation just like me... Unless you manually populate people in auth_user using the admin interface or some other method...
celopes
See my edit to the question. Following the Django code closely reveals that they expect the user to be modified in the authenticate method. I'm trying to figure out what I am doing wrong...
celopes
I'm selecting your answer because you provide a link to a working example. It looks like modifying the user instance in `authenticate()` like we are both doing is the only way to go. My answer below has more info on why I was getting a unique constraint violation. Thanks for your help.
celopes
+1  A: 

This will be a two part answer to my own question.

  1. What is the right place to plug in changes to the user instance during the login process?

    Judging from the Django code, my current implementation, and thraxil's answer above, I can only assume that it is expected and OK to modify the user instance in a custom authenticate() method.

    It smells wrong to me, as I said in my question, but the django code clearly assumes that it is possible that a user instance will be modified and I can find no other hooks to apply changes to the user model AFTER authentication, elsewhere.

    So, if you need an example, look at thraxil's code - in the selected answer to my question.

  2. Why my implementation is working differently from thraxil's and generating a unique constraint violation?

    This one was rather nasty to figure out.

    There is absolutely nothing wrong with Django. Well, if it already supported multiple databases (it is coming, I know!!!) I probably wouldn't have the problem.

    I have multiple databases, and different applications connect to one or more different ones. I'm using SQL Server 2005 (with django_pyodbc). I wanted to share the auth_user table between all my applications.

    With that in mind, what I did was create the auth models in one of the databases, and then create SQL Server synonyms for the tables in the other databases.

    It works just fine: allowing me to, when using database B, select/insert/update/delete from B.dbo.auth_user as if it were a real table; although what is really happening is that I'm operating on A.dbo.auth_user.

    But it does break down in one case: to find the generated identity, django_pyodbc does a:

    SELECT CAST(IDENT_CURRENT(%s) as bigint) % [table_name]

    and that doesn't appear to work against synonyms. It always returns nulls. So when in my authenticate() method I did user.save(), the saving part worked fine, but the retrieval of the identity column didn't - it would keep the user instance with a id of None, which would indicate to the django code that it should be inserted, not updated.

    The workaround: I had two choices:

    a) Use views instead of synonyms (this is what I did)

    b) Reload the user right after a user.save() using User.objects.get(username=username)

Hope that might help someone else.

celopes