I would like to enable the visitors of my website to login using their Google Accounts instead of having to sign up and create a new one.
A few things:
I tried to study the python-openid library + the google federated login API but I am lost. I get as close as to understand the instantiating the Consumer class but do not understand the session and store params required. I cannot fathom something that seems so easy can be so complicated. Is there really no step by step tutorial of how to do this in pure python or django?
I tried to look at the examples/consumer.py within the python-openid but it's 500lines of code again that I do not understand.
I also don't understand how verification of the user against google accounts is done on every request to my website. Google API only explains initial login steps. What happens on every request to my website where authentication must be verified against a google server?
I think your problem stems from a basic misunderstanding of how OpenID and/or OAuth work.
It looks like you just want authentication, so let's stick to OpenID for now. You are correct to look at existing libraries. python-openid is the one to use if you only need OpenID and not OAuth, and you are not using Django's built-in auth framework.
The full documentation for Federated Login with OpenID and OAuth is here: http://code.google.com/apis/accounts/docs/OpenID.html . In particular, look at the diagram under "Interaction sequence".
First, here is a very good working example from Facebook's Tornado web server's auth module:
https://github.com/facebook/tornado/blob/master/tornado/auth.py (grep that for "GoogleHandler". I've 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 that's still not enough, read on...
You said django-openid is irrelevant, but in fact it demonstrates an implementation of exactly what you want, but for Django's auth system instead of yours. Actually, you should look at the similar plugin, Django-SocialAuth, which implements OpenID + OAuth for a few different providers (Google, Facebook, Twitter, etc.). In particular, look at:
https://github.com/agiliq/Django-Socialauth/blob/master/socialauth/lib/oauthgoogle.py and https://github.com/agiliq/Django-Socialauth/tree/master/openid_consumer and https://github.com/agiliq/Django-Socialauth/tree/master/example_project
...for a full working example using django's auth framework, and can be adapted to your custom auth framework.
Best of luck. I encourage you to document whatever ends up working for you and build a step-by-step guide for others like you.
I have managed to demistify the problem so here is the solution and I hope someone else can benefit from it: 1) Google Account verification is not done against the google accounts server on every request to your application. For example: 1.1 a user logs into your app using their gmail account 1.2 the user also navigates to gmail.com where they check their email 1.3 they log out of gmail 1.4 they remain logged into your application and can use it fully This means you have to take care of session expiry on your end, Google account does not take care of it.
2) The core Python code 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, e.g. 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's 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's 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) an additional lib I created and imported above (myapp.common.util.openid) is a merge of a few existing Django openID libs so kudos to those 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 in order to hold 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With