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.

— by Robert Thomson, created 9th Jun, 2009, last modified 18th Jun, 2009 | Tags: Tech