Zope 3's Pluggable Authentication Utility doesn't automatically store a cookie saying that you're logged in if you've been authenticated once. Instead, it uses credentials and authentication plugins. For each credentials plugin, it passes the credential's output to each of the authentication plugins, and if any succeed, then it returns a Principal. It sounds logical enough, except that it does that every time, instead of storing a variable in my session stating that I'm already authenticated. Since I'll be authenticating against an external system, I didn't like the idea of checking the password on every request.. It wasn't immediately obvious to me how I could simply achieve this within the Pluggable Authentication framework, and I wasn't sure I needed the power of it anyway, so I decided to create my own IAuthentication implementation, and forget about the PAU altogether.
What it does
This implementation stores all the IPrincipal information (id, name, and description) as a signed cookie in the user's browser. By default, the session will expire after 6 hours of non-use, and the cookie's timestamp will be updated every 5 minutes with a new timestamp.
The Code
The signed string code is located in securestring.py, and the authentication code in auth.py. You also need to implement the authentication in your login form, and add it as a local utility to your application/site.
securestring.py
# coding: utf-8
import md5, random
SECRET='SomeRandomStringThatYouShouldNotShare' # CHANGE THIS
def make_sstring(question, string, r = None):
if r is None:
r = ''.join([random.choice('1234567890abcdef') for x in '12345678'])
m = md5.md5(SECRET + question + ":" + string + r)
return "%s|%s|%s" % (string, r, m.hexdigest())
def get_sstring(question, securestring):
string, r, md5 = securestring.split('|',2)
if make_sstring(question, string, r) == securestring:
return string
return False
auth.py
# coding: utf-8
from zope.app.security.interfaces import IAuthentication, IUnauthenticatedPrincipal, PrincipalLookupError, IPrincipal, ILogout
from zope import interface, schema, security
from securestring import make_sstring, get_sstring
from zope.app.component import hooks
from zope.traversing.browser.absoluteurl import absoluteURL
import time
from urllib import urlencode
class Principal(object):
interface.implements(IPrincipal)
def __init__(self, id, title, description):
self.id = id
self.title = title
self.description = description
def __str__(self):
return "<Principal: %s>" % self.title
def make_authenticated(request, principal):
id = principal.id
title = (principal.title or '').replace("::","..")
description = (principal.description or '').replace("::","..")
tm = int(time.time())
sstring = "%d::%s::%s::%s" % (tm, id, title, description)
sstring = make_sstring('z3c_sstring_login', sstring)
request.response.setCookie('z3c_sstring_login', sstring, path="/")
return principal
class SStringAuthenticator(object):
interface.implements(IAuthentication, ILogout)
loginpagename = 'login'
timeout_in_seconds = 60*60*6 # 6 hours
update_timeout = 60*5 # how often to update the cookie
def logout(self, request):
request.response.expireCookie('z3c_sstring_login', path="/")
def authenticate(self, request):
sstring = request.cookies.get('z3c_sstring_login', None)
if sstring is None:
return None
sstring = get_sstring('z3c_sstring_login', sstring)
if not sstring:
return None
try:
tm, id, title, description = sstring.split('::',3)
tm = int(tm)
now = int(time.time())
if (now - tm) < self.timeout_in_seconds:
principal = Principal(id, title, description)
if (now-tm) > self.update_timeout:
make_authenticated(request, principal)
return principal
except:
pass
def unauthenticatedPrincipal(self):
# not really sure what to do here, but it doesnt seem to hurt
return None
def unauthorized(self, id, request):
site = hooks.getSite()
stack = request.getTraversalStack()
stack.reverse()
query = request.get('QUERY_STRING')
camefrom = '/'.join([request.getURL(path_only=True)] + stack)
if query:
camefrom = camefrom + '?' + query
url = '%s/@@%s?%s' % (absoluteURL(site, request),
self.loginpagename,
urlencode({'camefrom': camefrom}))
request.response.redirect(url)
def getPrincipal(self, id):
principal = Principal(id, id, id)
interface.directlyProvides(principal, IUnauthenticatedPrincipal)
return principal
Install it as a local utility
In your application, you can add it as a local utility. Since I'm using Grok, I'll give a Grok example:
class MyApp(grok.Application)
grok.local_utility(SStringAuthenticator, provides=IAuthentication)
You could of course also provide a setup function, to modify the string.
Local Utilities will only appear on NEW objects, so your existing applications/sites won't make use of it.
Authenticate from your login form
I wasn't sure the best way to do the actual authentication here, so I thought I'd just leave it up to the login form. If successful, the login form should just call auth.make_authenticated(request, principal) where Principal is an instance of an IPrincipal (you can use auth.Principal if you like, but there's no doubt a better way).
The Alternative using Pluggable Auth
I realised afterwards that it would be possible to do the same thing within the pluggable auth framework. You could do it almost exactly the same way, except that the credentials plugin should just return the cookie if it's set (credentials just returns a dict, so you're not limited to just a login/password), and the authentication plugin can just check if it's valid. The credentials plugin can still redirect unauthorized users to a login page. http://grok.zope.org/documentation/how-to/authentication-with-grok should be able to give you an idea about how to implement Pluggable Authentication.