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.