App Engine User handling on Django Nonrel

After some more tinkering and vacillating between Django Nonrel and App Engine Helper, I decided to work with the former for a bit and it’s looking like the right decision.

I’ve struggled to find time over the last week or two to properly look at these issues, but yesterday I had nearly a full day to sit down and try to get to grips with things. I’ve said already that SocialAuth, while looking very cool, didn’t quite do what I needed, so it was time to sit down and write something equivalent.

I wasn’t sure if I’d be able to use App Engine’s User’s API or not, but it turned out that using Nonrel meant that Django’s auth system and Google’s were kept nicely separate and I could plumb them together at my leisure (exactly as I needed). I think that if I’d tried to use the App Engine Helper I would’ve come badly unstuck.

First up some models


class CommonProfileInfo(models.Model):
    serviceprovider = models.ForeignKey(ServiceProvider)
    user = models.ForeignKey(User, related_name='%(class)s_profiles') # (django.contrib.auth.models.User)
    class Meta:
        abstract = True

    def __str__(self):
        return u"%s's Profile"%(screen_name or "Unidentified")

class GoogleProfile(CommonProfileInfo):
    google_user_id = models.CharField() # we don't store a 'user' object, we just hold the user account's id (google.appengine.api.users)

    screen_name = models.CharField(blank=True, null=True)
    email = models.EmailField(blank=True, null=True)

Originally I didn’t seem to be able to use Django’s %(class) specifier for related_name. Wasn’t sure why. As Waldemar Kornewald kindly pointed out in a comment, this was just because of a stupid typo: %(class)s. Thanks! Fixed & working.

Next up, a view cobbled together for test purposes from the Google login example:

def googlelogin(request):
    from google.appengine.api import users

    google_user = users.get_current_user()
    if google_user:

        user = authenticate(request=request, google_user=google_user)
        login(request, user)

        html = ("Welcome, %s! (<a href=\"%s\">sign out</a>)" %
                    (google_user.nickname(), users.create_logout_url("/")))
    else:
        return redirect( users.create_login_url( reverse('myapp.views.googlelogin') ) )

    html = "<html><body>\n%s\n</body></html>"%(html)

    return HttpResponse(html)

And lastly we need an authentication backend to tie this all together. I was very pleasantly surprised at how easy this all was.

This has an important and slightly different job from normal – it might be that we’ve been asked to authenticate while having a user of our system already logged in. In this case we should remember this new profile of theirs. If this is a new, unrecognised profile and there’s no user then we create one.

class GoogleBackend:
    def authenticate(self, request, google_user):
        try:
            prof = GoogleProfile.objects.get(google_user_id=google_user.user_id())
            return prof.user
        except GoogleProfile.DoesNotExist:

            # ...[snip]... deal with our django user, email, nickname etc here

            # a new profile will need to be tied to an existing ServiceProvider, so get the correct instance (i.e. google) here
            sp = get_service_provider("google") # call utility method to get a model instance

            # create a new google profile
            newprofile = GoogleProfile.objects.create(
                serviceprovider = sp,
                user = user,
                google_user_id = google_user.user_id(),
                screen_name = nickname,
                email = email)
            newprofile.save()

            return user

Lastly tied together by specifying this new auth backend in settings

AUTHENTICATION_BACKENDS = ( 'myapp.auth_backends.GoogleBackend',
                            'django.contrib.auth.backends.ModelBackend',
                           )

This is only really prototype code for testing out the concept, there’ll be plenty of tidying up to do in short order, but the concept seemed to work just as intended.

Google API and Django auth system played nicely together. New auth backend was extremely easy to write (I’m really enjoying how easy Django makes it to hook things in). Proof of concept done and I can move on to some other auth mechanisms.

Advertisements

About pycruft

Meandering through a codified whimsy
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

8 Responses to App Engine User handling on Django Nonrel

  1. Regarding your ‘%(class)_profiles’ problem: I think you’re just missing the ‘s’: ‘%(class)s_profiles’.

    You could also take a look at the g_helpers package which provides backends for App Engine User authentication: http://bitbucket.org/d3f3nd3r/djangoappengine-helpers

    • pycruft says:

      Ah, you’re quite right about the %(class) thing – a dim typo. Thanks!

      Thanks for the djangoappengine-helpers link but it has the same issue for me as USWare’s Socialauth – it’s actually very well coupled to the Django auth system, with a 1:1 mapping from google users to django, which isn’t what I’m after in this case: hence and the hacking about rolling my own.

  2. Guy says:

    Hi,
    would you kindly share some more of the example code to make this article more complete? (e.g. How the CommonProfileInfo model is used by other models, what is the role of the ServiceProvider class, and such?)

    I am a new Django/Python/App engine developer so some of the details still escape me.

    Thank you very much!
    Guy.

    • pycruft says:

      A couple of the things I’m trying to work on at the moment are a broader django-on-app-engine introduction and finishing off this user handling stuff to put online as an example library/app.

      Unfortunately Real Life is getting in the way – this is a sideline and real work is taking up a bit too much time.

      I’m wary of dumping the code I currently have online because it needs some significant fixing & reworking – I wouldn’t want anyone else follow its example at the moment! (One of the things I’m very likely to change is this ‘Profile’ code, since it’s rather ugly).

      Please remember, when reading the above, that it’s all prototypical/exploratory code!

      To briefly explain what’s going on above, however: CommonProfileInfo is an abstract base class from which my real profile classes (e.g. for Google or Oauth) are inheriting. These are models so they translate into database tables (or entity types, in the google datastore). Because it’s an abstract base it is essentially a set of extra fields which get bolted on to the subclasses.

      So, GoogleProfile gets a User and a ServiceProvider. I have a few Profile classes and Common..Info is just a useful place to dump all their common attributes.

      How does this get used? You’ll notice that in the GoogleBackend authenticate() method we look the profile up based on ‘google_user’ or create a new one if we don’t have one that matches.

      The authenticate() function itself gets called from the googlelogin() view. These aren’t tied directly together, however: the contrib.auth function loops through all its registered ‘AUTHENTICATION_BACKENDS’ until it finds one which takes the correct parameters (i.e. google_user) and returns a User object. This decoupling of auth backend and view logic is one of the things that lets us easily swap or support multiple auth mechanisms.

      You might also gain some insight from some of the issues I had: Why is my auth backend silently failing?.

      If you’re interested in just fairly ‘normal’ user auth/registration, Socialauth and Django-Registration are worth investigating.

      • Guy says:

        Thanks for your quick (and detailed) response!
        After reading it some things make more sense now, and the whole flow is a little clearer.
        I’m still in the dark about the ServiceProvider class. I can see you creating an instance via a helper method in the authenticate method, but I don’t understand how or when it is used.
        It would be great if you could shed some light on this topic as well.

        Thanks again,
        Guy.

  3. pycruft says:

    The ServiceProvider class can be more or less ignored – it just holds some info about the service we’re using to authenticate users (e.g. Twitter or Google), for example the name of the servive for display and a url: it’s really only used for display and for ensuring that we don’t confuse identical usernames from separate providers.

    Thanks for the feedback tho – I’m going to tweak the code comments slightly because “# create a new google profile” is rather confusing just there – the line following it doesn’t create anything, it looks up an existing ServiceProvider instance!

  4. Adam Alton says:

    In your first code snippet, where are you importing User from? The comment says “we don’t store a ‘db.user’ object, we just hold the user account’s id”, but in the CommonProfileInfo class you’re storing a ForeignKey to User. Is that django.contrib.auth.models.User ? And if so, does the authenticate method of your GoogleBackend class return a User object?

    • pycruft says:

      Yes, there are rather too many ‘user’s floating around and it’s rather confusing. “User” in the first snippet is indeed a contrib.auth.User (and is imported further up – these are all just snippets).

      The comment about not storing a user object applies to user object from Google’s API. This is a confusing comment (which I’ll now edit) because it’s a appengine.api.users.User object there’s nothing “db” about it.

      Authentication methods, like the one in GoogleBackend, must return a User object, see the django auth-backend docs. The one in this example (just called ‘user’) is created in a messy bit of code (snipped out) which deals with complications of whether or not we currently have a logged in django user and what nickname/email address, if any, to use.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s