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.