Friday, 1 March 2013

Implementing OpenID authentication with Cherrypy

Originally published: 2010-05-09 on my old blog

Seeing as I've just spent most of the weekend trying to get OpenID authentication working on richbeales.net, I thought I'd share how I did it, and some pitfalls I encountered along the way.
Installation was easy, and the documentation on both OpenID and python-openid is reasonably good, however there seems to be a lack of sample code.  API references are all well and good, but they don't show you *exactly* how it should be done.
So, for anyone attempting the same thing, I'm going to reproduce the relevant sections of my login script for Richbeales.net so that others don't have to waste their weekends. 
Firstly, you'll need to install python-openid itself, which seems to be quite well distributed, so on my debian-based system, this was as easy as "apt-get install python-openid".  So, with that installed, and your website already up and running with Python and some sort of web framework (Cherrypy in this case), you'll need to do something like the following:

#! /usr/bin/python
import cherrypy, logging
from openid.consumer.consumer import Consumer, AuthRequest, SuccessResponse
from openid.store.memstore import MemoryStore
class Login(object):
def __init__(self):
self.store = MemoryStore()
self.returnurl = ''
@cherrypy.expose
def default(self, *args, **kwargs):
if len(args) == 1:
self.returnurl = args[0]
return self.PrintLoginForm()
@cherrypy.expose
def index(self):
return self.PrintLoginForm()
@cherrypy.expose
def do(self,openid_url='',returnurl='',loginsubmit=''):
self.returnurl = returnurl
consumer = Consumer(cherrypy.session, self.store)
username = self.FormatUserName(openid_url)
cherrypy.log('initialised consumer', context='', severity=logging.DEBUG, traceback=False)
auth = consumer.begin(username)
cherrypy.log('begin called', context='', severity=logging.DEBUG, traceback=False)
auth.addExtensionArg('sreg','type.email','')
newurl = auth.redirectURL('http://*.mysite.com','http://www.mysite.com/login/verify')
cherrypy.log('redirect called to '+ newurl, context='', severity=logging.DEBUG, traceback=False)
raise cherrypy.HTTPRedirect(newurl)
@cherrypy.expose
def verify(self, *args, **kwargs):
try:
cherrypy.log('verifying ' + str(args) + str(kwargs), context='', severity=logging.DEBUG, traceback=False)
consumer = Consumer(cherrypy.session, self.store)
completedict = {'openid.mode':'check_authentication'}
for k,v in kwargs.iteritems():
completedict[k] = v
result = consumer.complete(completedict,'http://www.mysite.com/login/verify?janrain_nonce=' + completedict['janrain_nonce'])
if type(result) is SuccessResponse:
cherrypy.log('complete success' + str(result.signed_fields), context='', severity=logging.DEBUG, traceback=False)
username = self.FormatUserName(result.identity_url)
#
# Do your site-specific login here
#
return self.PrintSuccess()
else:
cherrypy.log('complete failure ' + str(result), context='', severity=logging.DEBUG, traceback=False)
return self.PrintFailure(result.message)
except Exception, inst:
cherrypy.log(str(inst), context='', severity=logging.DEBUG, traceback=False)
return self.PrintFailure()
def FormatUserName(self, usrname):
return usrname.strip().replace('http://','').rstrip('/')
def PrintSuccess(self):
return "<a href="/%s">Proceed</a>" % (self.returnurl)
def PrintFailure(self, extrainfo = ''):
return "Failed to log in - auth failed %s <a href="/login/%s">Try Again</a>" % (extrainfo, self.returnurl)
def PrintLoginForm(self):
return """
<p>Please log in using OpenID</p>­
<form action="/login/do" method="post">
<label for="openid_url">Your Open ID: </label><input name="openid_url" id="openid_url" style="background: transparent url(/img/openid-inputicon.gif) no-repeat" type="text">
<a href="http://www.openid.net">Get an Open ID</a>
<input name="loginsubmit" value="Log In" type="submit">
<input name="returnurl" value="%s" type="hidden">
</form>
""" % (self.returnurl)
­
One thing that tripped me up for a while was the "realm" parameter to auth.RedirectURL, I was simply putting "mysite.com" whereas the real answer was "http://*.mysite.com", again not something you'll find out just by reading the API docs.  Obviously replace mysite.com with your own domain name whereever you see it.

I've still got a little way to go (like retrieving nicknames and email addresses from the OpenID provider), but the above will certainly work for authentication.  Good luck (and let me know if you can help with the email retrieval!  Cheers.



­