Django + Google Federated Login

I want visitors to my website to be able to log in using their Google accounts, instead of registering and creating a new one.

A few things:

  • I DO NOT use the Django authentication environment; instead, I perform my own authentication and store user information in my own set of tables.
  • therefore, the various django-openid libraries are not applicable, since they all assume that the standard Django framework is used.

I tried to learn the python-openid + library API for integrating with Google, but I lost. I understand how to create an instance of the Consumer class, but I don’t understand what is needed for the session and saving the parameters. I can’t understand what seems so easy, can be so complicated. Is there really no step-by-step guide on how to do this in pure python or django?

I tried looking at the /consumer.py examples inside python-openid, but these are another 500 lines of code that I don't understand.

I also do not understand how user verification with Google accounts is performed for each request to my site. The Google API only explains the initial login steps. What happens with every request of my website where authentication should be verified on google server?

+4
source share
2 answers

I managed to ease the problem, so here is the solution, and I hope someone can benefit from it: 1) Google account verification is not performed on the google accounts server for every request of your application. For example: 1.1 the user logs into your application using his gmail account 1.2 the user also moves to gmail.com where they check their email 1.3 they exit gmail 1.4 they remain in your application and can use it fully This means that you you need to take care of ending the session at the end, your Google account will not take care of it.

2) The Python core that I used is the following:

from openid.consumer.consumer import Consumer, \ SUCCESS, CANCEL, FAILURE, SETUP_NEEDED from openid.consumer.discover import DiscoveryFailure from django.utils.encoding import smart_unicode from myapp.common.util.openid import DjangoOpenIDStore def google_signin(request): """ This is the view where the Google account login icon on your site points to, eg http://www.yourdomain.com/google-signin """ consumer = Consumer(request.session, DjangoOpenIDStore()) # catch Google Apps domain that is referring, if any _domain = None if 'domain' in request.POST: _domain = request.POST['domain'] elif 'domain' in request.GET: _domain = request.GET['domain'] try: # two different endpoints depending on whether the using is using Google Account or Google Apps Account if _domain: auth_request = consumer.begin('https://www.google.com/accounts/o8/site-xrds?hd=%s' % _domain) else: auth_request = consumer.begin('https://www.google.com/accounts/o8/id') except DiscoveryFailure as e: return CustomError(request, "Google Accounts Error", "Google OpenID endpoint is not available.") # add requests for additional account information required, in my case: email, first name & last name auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'mode', 'fetch_request') auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'required', 'email,firstname,lastname') auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'type.email', 'http://schema.openid.net/contact/email') auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'type.firstname', 'http://axschema.org/namePerson/first') auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'type.lastname', 'http://axschema.org/namePerson/last') return redirect(auth_request.redirectURL('http://www.yourdomain.com', 'http://www.yourdomain.com/google-signin-response'))) @transaction.commit_manually def google_signin_response(request): """ Callback from Google Account service with login the status. Your url could be http://www.yourdomain.com/google-signin-response """ transaction.rollback() # required due to Django transaction inconsistency between calls oidconsumer = Consumer(request.session, DjangoOpenIDStore()) # parse GET parameters submit them with the full url to consumer.complete _params = dict((k,smart_unicode(v)) for k, v in request.GET.items()) info = oidconsumer.complete(_params, request.build_absolute_uri().split('?')[0]) display_identifier = info.getDisplayIdentifier() if info.status == FAILURE and display_identifier: return CustomError(request, _("Google Login Error"), _("Verification of %(user)s failed: %(error_message)s") % {'user' : display_identifier, 'error_message' : info.message}) elif info.status == SUCCESS: try: _email = info.message.args[('http://openid.net/srv/ax/1.0', 'value.email')] _first_name = info.message.args[('http://openid.net/srv/ax/1.0', 'value.firstname')] _last_name = info.message.args[('http://openid.net/srv/ax/1.0', 'value.lastname')] try: _user = User.objects.get(email__iexact=_email) except ObjectDoesNotExist: # create a new account if one does not exist with the authorized email yet and log that user in _new_user = _new_account(_email, _first_name + ' ' + _last_name, _first_name, _last_name, p_account_status=1) _login(request, _new_user, info.message.args[('http://specs.openid.net/auth/2.0', 'response_nonce')]) transaction.commit() return redirect('home') else: # login existing user _login(request, _user, info.message.args[('http://specs.openid.net/auth/2.0', 'response_nonce')]) transaction.commit() return redirect('home') except Exception as e: transaction.rollback() system_log_entry(e, request=request) return CustomError(request, _("Login Unsuccessful"), "%s" % e) elif info.status == CANCEL: return CustomError(request, _("Google Login Error"), _('Google account verification cancelled.')) elif info.status == SETUP_NEEDED: if info.setup_url: return CustomError(request, _("Google Login Setup Needed"), _('<a href="%(url)s">Setup needed</a>') % { 'url' : info.setup_url }) else: # This means auth didn't succeed, but you're welcome to try # non-immediate mode. return CustomError(request, _("Google Login Setup Needed"), _('Setup needed')) else: # Either we don't understand the code or there is no # openid_url included with the error. Give a generic # failure message. The library should supply debug # information in a log. return CustomError(request, _("Google Login Error"), _('Google account verification failed for an unknown reason. Please try to create a manual account on Acquee.')) def get_url_host(request): if request.is_secure(): protocol = 'https' else: protocol = 'http' host = escape(get_host(request)) return '%s://%s' % (protocol, host) 

3) the additional lib that I created and imported above (myapp.common.util.openid) is a merge of several existing Django openID libraries, so desired for these guys:

 from django.db import models from django.conf import settings from django.utils.hashcompat import md5_constructor from openid.store.interface import OpenIDStore import openid.store from openid.association import Association as OIDAssociation import time, base64 from myapp.common.db.accounts.models import Association, Nonce class DjangoOpenIDStore(OpenIDStore): """ The Python openid library needs an OpenIDStore subclass to persist data related to OpenID authentications. This one uses our Django models. """ def storeAssociation(self, server_url, association): assoc = Association( server_url = server_url, handle = association.handle, secret = base64.encodestring(association.secret), issued = association.issued, lifetime = association.issued, assoc_type = association.assoc_type ) assoc.save() def getAssociation(self, server_url, handle=None): assocs = [] if handle is not None: assocs = Association.objects.filter( server_url = server_url, handle = handle ) else: assocs = Association.objects.filter( server_url = server_url ) if not assocs: return None associations = [] for assoc in assocs: association = OIDAssociation( assoc.handle, base64.decodestring(assoc.secret), assoc.issued, assoc.lifetime, assoc.assoc_type ) if association.getExpiresIn() == 0: self.removeAssociation(server_url, assoc.handle) else: associations.append((association.issued, association)) if not associations: return None return associations[-1][1] def removeAssociation(self, server_url, handle): assocs = list(Association.objects.filter( server_url = server_url, handle = handle )) assocs_exist = len(assocs) > 0 for assoc in assocs: assoc.delete() return assocs_exist def useNonce(self, server_url, timestamp, salt): # Has nonce expired? if abs(timestamp - time.time()) > openid.store.nonce.SKEW: return False try: nonce = Nonce.objects.get( server_url__exact = server_url, timestamp__exact = timestamp, salt__exact = salt ) except Nonce.DoesNotExist: nonce = Nonce.objects.create( server_url = server_url, timestamp = timestamp, salt = salt ) return True nonce.delete() return False def cleanupNonce(self): Nonce.objects.filter( timestamp__lt = (int(time.time()) - nonce.SKEW) ).delete() def cleaupAssociations(self): Association.objects.extra( where=['issued + lifetimeint < (%s)' % time.time()] ).delete() def getAuthKey(self): # Use first AUTH_KEY_LEN characters of md5 hash of SECRET_KEY return md5_constructor.new(settings.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN] def isDumb(self): return False 

4) and the model that is required to store google account session identifiers and verified endpoints:

 class Nonce(models.Model): """ Required for OpenID functionality """ server_url = models.CharField(max_length=255) timestamp = models.IntegerField() salt = models.CharField(max_length=40) def __unicode__(self): return u"Nonce: %s for %s" % (self.salt, self.server_url) class Association(models.Model): """ Required for OpenID functionality """ server_url = models.TextField(max_length=2047) handle = models.CharField(max_length=255) secret = models.TextField(max_length=255) # Stored base64 encoded issued = models.IntegerField() lifetime = models.IntegerField() assoc_type = models.TextField(max_length=64) def __unicode__(self): return u"Association: %s, %s" % (self.server_url, self.handle) 

Good luck Rok

+3
source

I think your problem is related to a basic misunderstanding of OpenID and / or OAuth.

It sounds like you just want authentication, so open OpenID. You look at existing libraries correctly. python-openid is the one you need if you only need OpenID and not OAuth and you do not use Django built-in autoload.

Full documentation for Federated Login with OpenID and OAuth is available here: http://code.google.com/apis/accounts/docs/OpenID.html . In particular, look at the diagram in the “Interaction Sequence” section.

Firstly, here is a very good working example from the Facebook Tornado web server:

https://github.com/facebook/tornado/blob/master/tornado/auth.py (grep that for "GoogleHandler". I have used it with great success.) This is independent of Django and Django auth and should give you a good example of how to implement what you want. If this is not enough, read on ...

You said that django-openid does not matter, but in fact it demonstrates the implementation of exactly what you want, but for the Django auth system instead of yours. In fact, you should look at a similar plugin, Django-SocialAuth , which implements OpenID + OAuth for several different providers (Google, Facebook, Twitter, etc.). In particular, see:

https://github.com/agiliq/Django-Socialauth/blob/master/socialauth/lib/oauthgoogle.py as well as https://github.com/agiliq/Django-Socialauth/tree/master/openid_consumer as well as https: //github.com/agiliq/Django-Socialauth/tree/master/example_project

... for a complete working example using the django auth framework and can be adapted to your custom auth structure.

Good luck. I recommend that you document everything that works for you and create a walkthrough for others like you.

+10
source

All Articles