Personal tools
You are here: Home Documentation How-Tos Authentication with Grok

Authentication with Grok

Warning: This item is marked as outdated.

This How-to applies to: 1.0a1, 0.14, 0.13
This How-to is intended for: Developer, Advanced Developer

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!

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.

local_utility not working

Posted by Andreas Jung at Jan 12, 2009 02:05 AM
My app class looks likes this:

class Osa(grok.Application, grok.Container):
    grok.local_utility(
        PluggableAuthentication, provides=IAuthentication,
        setup=setup_authentication,
        )

However setup_authentication is never called (according to pdb and debugging code)?!

local_utility

Posted by Martijn Faassen at Jan 12, 2009 09:20 AM
It will only be called if you install a *new* application - local_utility doesn't affect existing application objects. We'd *like* there to be a feature in Grok that would help with the installation of utilities in existing objects, but it isn't there yet. So does it work when you install a new app or do you still have a problem?

session credential plugin and login form

Posted by Steve Wang at Mar 26, 2009 12:03 AM
Could you kindly explain in a little more detail how "The session credentials plugin will automatically redirect the user to this login form when the credentials cannot be found in the session.".

I'm not getting the login form displayed in my test app. Do I put the login form code in file login.py in the same directory app.py lives? Assuming I create a brand new application called 'a', and I can get to localhost:8080/a, what do I need to type in the URL in order to see that the session directs me to the login form? I'm not sure I understand how the application code in app.py relates to and knows about the login form.

Thank you very much.

re: session credential plugin and login form

Posted by Uli Fouquet at Apr 24, 2009 04:50 AM
You have to create a 'protected', i.e. non-public page first, of course.

You can do so for instance by using:

  grok.require('zope.manage')

in a certain view.

setting timeout for authenticated users

Posted by wubin at May 22, 2012 10:48 AM
I was trying to modify the default timeout value for every authenticated user in the application. Some said adding 'session-timeout-minutes' in the zope.conf but it did not work on me.(It says that 'session-timeout-minutes' is not a known key name) I will appreciate if anyone would help me out.