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... 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.