Authentication with Grok

Author:Martijn Faasen (faassen)
Version:unkown

This document tries to explain how to set up custom authentication against your own database (may be it ZODB, a relational database or LDAP) with Grok. It doesn’t go into the details of how to query the database, but shows the bits and pieces you need to integrate with Grok.

Note that the situation is currently rather low-level for Grok and that this document is rather coarse. Contributions to the document and to the authentication situation (in the form of helpful libraries) would be very welcome!

Note

On grok 1.4, you need to depend your egg on the zope.app.authentication and zope.app.security packages.

First we’re going to set up an application with custom authentication:

import grok

from zope.app.authentication.authentication import PluggableAuthentication
from zope.app.security.interfaces import IAuthentication

class MyApplication(grok.Application, grok.Model):
    grok.local_utility(
        PluggableAuthentication, provides=IAuthentication,
        setup=setup_authentication,
        )

When the application is installed, a local utility will be automatically installed into it. When this local utility is installed, the setup_authentication function will be called to further configure it. Let’s implement that now:

def setup_authentication(pau):
    """Set up plugguble authentication utility.

    Sets up an IAuthenticatorPlugin and
    ICredentialsPlugin (for the authentication mechanism)
    """
    pau.credentialsPlugins = ['credentials']
    pau.authenticatorPlugins = ['users']

Session-based login

The pluggable authentication system needs at least one credentials plugin, which is responsible for extracting credentials from the user’s request, and one authenticator plugin, which authenticates the credentials against an actual user (principal).

Here we’ve configured the pluggable authentication utility to look up an ICredentialsPlugin utility with the name credentials and a IAuthenticatorPlugin with the name users.

We need to supply these utilities next. In our example we’re both going to make them global utilities so they need no special setup in the MyApplication object above. If you need them to store data or configuration information in the ZODB however you should create them as a local utility (and also probably provide a user interface for them). This is left as an exercise to the reader.

Our credentials plugin will use the persistent user-specific session to retrieve credentials information - it’s like cookie-based login:

from zope.app.authentication.session import SessionCredentialsPlugin
from zope.app.authentication.interfaces import ICredentialsPlugin

class MySessionCredentialsPlugin(grok.GlobalUtility, SessionCredentialsPlugin):
    grok.provides(ICredentialsPlugin)
    grok.name('credentials')

    loginpagename = 'login'
    loginfield = 'form.login'
    passwordfield = 'form.password'

Session-based login needs to know about a special login page where the user fills their username and password in a form, so that it can retrieve the information to store in a session. We need to supply it with the name of the form (login) and the name of the form fields.

Let’s set up this login page next, using the grok.Form mechanism:

from zope.interface import Interface
from zope import schema

class ILoginForm(Interface):
    login = schema.BytesLine(title=u'Username', required=True)
    password = schema.Password(title=u'Password', required=True)

class Login(grok.Form):
    grok.context(Interface)
    grok.require('zope.Public')

    form_fields = grok.Fields(ILoginForm)

    @grok.action('login')
    def handle_login(self, **data):
        self.redirect(self.request.form.get('camefrom', ''))

The session credentials plugin will automatically redirect the user to this login form when the credentials cannot be found in the session. It’s available on all objects as it’s registered for Interface. When the user fills in the credentials they will appear as form.login and form.password in the request where the credentials plugin can find them and store them in the session.

After submitting the form successfully the user is immediately redirected to the URL in camefrom. This camefrom variable is automatically added to request when the login form is rendered and is the original page the user tried to go to when they were redirected to this one (because a login was required first). In the login.pt template we need to make sure we retrieve it so that it is submitted along with the rest of the form. Let’s therefore look at a bit of the login.pt template:

...code to render the formlib form...
  <input tal:condition="request/camefrom | nothing" type="hidden"
       name="camefrom" tal:attributes="value request/form/camefrom | nothing" />

It’s also nice to have a logout page:

from zope.app.security.interfaces import (IAuthentication,
                                          IUnauthenticatedPrincipal,
                                          ILogout)

class Logout(grok.View):
    grok.context(Interface)
    grok.require('zope.Public')

    def update(self):
        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
            auth = component.getUtility(IAuthentication)
            ILogout(auth).logout(self.request)

This page logs out the user if it’s an authenticated user (principal).

Authentication

That’s what is needed for retrieving user credentials. Let’s now look at how we can authenticate the user next. For this we need to set up an IAuthenticatorPlugin with the name users:

from zope.app.authentication.interfaces import IAuthenticatorPlugin

class UserAuthenticatorPlugin(grok.GlobalUtility):
    grok.provides(IAuthenticatorPlugin)
    grok.name('users')

    def authenticateCredentials(self, credentials):
        if not isinstance(credentials, dict):
            return None
        if not ('login' in credentials and 'password' in credentials):
            return None
        account = self.getAccount(credentials['login'])

        if account is None:
            return None
        if not account.checkPassword(credentials['password']):
            return None
        return PrincipalInfo(id=account.name,
                             title=account.name,
                             description=account.name)

    def principalInfo(self, id):
        account = self.getAccount(id)
        if account is None:
            return None
        return PrincipalInfo(id=account.name,
                             title=account.name,
                             description=account.name)

    def getAccount(self, login):
        ... look up the account object and return it ...

What you need to do is implement the getAccount method to return an instance of an account object. This object should provide a name attribute which is the login name under which the account is used, and a checkPassword method which can be used to check the password. If no such account can be found, None must be returned. In getAccount you should consult the proper database, such as the ZODB, or an LDAP database, or a relational database, so that the proper account object can be retrieved or constructed.

The structure of this particular authenticator plugin is just one example of course - you can rewrite it to suit your particular user database system. You may for instance want to separate out checkPassword from the account object.

There are a few bits and pieces still needed, such as the PrincipalInfo class referred to above:

from zope.app.authentication.interfaces import IPrincipalInfo

class PrincipalInfo(object):
    grok.implements(IPrincipalInfo)

    def __init__(self, id, title, description):
        self.id = id
        self.title = title
        self.description = description
        self.credentialsPlugin = None
        self.authenticatorPlugin = None

We’ll also give an example of an account object with password management facilities (encrypting the password):

from zope import component
from zope.app.authentication.interfaces import IPasswordManager

class Account(grok.Model):
    def __init__(self, name, password):
        self.name = name
        self.setPassword(password)

    def setPassword(self, password):
        passwordmanager = component.getUtility(IPasswordManager, 'SHA1')
        self.password = passwordmanager.encodePassword(password)

    def checkPassword(self, password):
        passwordmanager = component.getUtility(IPasswordManager, 'SHA1')
        return passwordmanager.checkPassword(self.password, password)

Instead of a grok.Model which allows its storage in the ZODB (such as in a container), you could construct this object on the fly when needed, or you could use an ORM mapper.