Single-Sign-On for Zope & Plone in Windows Domains
One of my clients runs his own intranet for his employees. The intranet currently consists of a bunch of static HTML pages which are written and maintained by a single editor. This approach might have been sufficient five years ago but as the intranet grew it became apparent that the static HTML would become unmanageable in the near future. I therefore suggested replacing the existing solution with a community-oriented CMS. Due to certain requirements the set of contestants was cut down to two: Drupal and Plone. I favored Plone because a) I am slightly disappointed with the direction and pace that Drupal’s development has recently taken and b) I wanted to learn Zope and Plone anyway.
Table Of Contents
State Of The Art
Step 1 - NTLM authentication for Apache:
mod_ntlm
Step 2 - Zope/Plone and Apache
Step 3 - FastCGI
Step 4 - Enable NTLM authentication for requests to Zope
Step 5 - Install Remote User Folder
Appendix 1 - Exempting the ZMI from NTLM authentication
Appendix 2 - Schizophrenia
Appendix 3 - Disabling Zope's http server
Appendix 4 - Virtual Host Monster (VHM)
Appendix 5 - What about Group User Folder?
Appendix 6 - Problems with mod_ntlm under load
Appendix 7 - Multiple Windows Domains
Appendix 8 - Kerberos
State Of The Art
The most important requirement was the integration of Plone’s user management into the client’s Windows domain. This requirement had two implications:
- Unified logon: Any domain user should automatically be a Plone user. There should not be any additional user administration in Plone.
- Seamless authentication: Users should only have to logon once. After a user is logged into the domain, Plone should not ask for a password a second time.
The above requirements are usually summarized as single-sign-on. This article describes how single-sign-on can be achieved by gluing together various pieces of software in addition to Zope/Plone; namely, Apache, mod_ntlm, mod_fastcgi, a Samba or Windows 2000 Server and Zope’s Remote User Folder.
In coming to my conclusions I examined several possible solutions:
- LDAPUserFolder could be used to fulfill the unified logon requirement by integrating Active Directory (Microsoft’s extended LDAP directory implementation for Windows domains) with Plone. However, with LDAPUserFolder the user would need to login into the CMS separately. This is basically because there is no trust relationship between the Zope Server that Plone runs on and the Windows workstation that the client logs into. Currently, such trust can only be established using Microsoft’s own proprietary HTTP authentication protocol, called NTLM authentication. NTLM auth was initially supported by Internet Explorer (IE) and Microsoft’s web server, Internet Information Server (IIS). The NTLM authentication scheme is sneered at by many because it’s thought to be rather weak (I remember reading somewhere that there is a stronger Kerberos extension to it). Notwithstanding, it has been adopted by a variety of open source projects, probably because of user demand and usefulness. The Squid proxy server supports it, Firefox 1.0 supports it and for Apache there is mod_ntlm. I successfully used all of these in conjunction with NTLM authentication in my client’s intranet. The security risk imposed by NTLM authentication in a controlled LAN environment is acceptable.
- The Plone product exUserFolder can authenticate against a windows domain but it doesn’t support NTLM authentication either.
- The Plone product PluggableAuthService (PAS) looks very promising but I doubt that at the time of this writing it’s ready for production or that it even supports NTLM auth. There were posts on the corresponding mailing list that indicate someone is working on NTLM auth though.
This leaves us with one remaining option: Running Zope behind Apache with mod_ntlm installed. The remainder of this article focuses on the details of how to get Apache 2.0 to do the authentication and yet let Zope serve the content for the Plone CMS. The procedure for Version 1.3 of the Apache web server should be very similar but is not addressed in this article.
Step 1 – NTLM authentication for Apache: mod_ntlm
Update, Feb 1st, 2005: Read this section and then immediately read Appendix 6!
Apache’s mod_ntlm is
available for Apache 1.3 and 2.0 and although still in beta it is pretty much usable
as long as you use it in one domain only. I prefer loading it as a module, which
is the default build setting. Download the modntlm2 source package [3] and
compile and install it according to the instructions found either on the home page [4] or
in the package itself. The compilation requires the Apache header files. The installation
uses the apxs program to install the compiled module but that didn’t work
on my Suse Linux system. If this is the case for your system too, there are two
things you will have to do. First, you will need to copy the mod_ntlm.so file to
the directory that contains the other Apache modules, e.g /usr/lib/apache2
.
Looking into httpd.conf for LoadModule
statements of other modules
should give you an idea.
LoadModule ntlm_module /usr/lib/apache2/mod_ntlm.so
The bold part will probably need to be adjusted to match your system’s module path. Restart Apache to see if it starts up correctly. Then add this to your httpd.conf:
<Location /ntlmtest> AuthType NTLM NTLMAuth on NTLMAuthoritative on NTLMDomain YOURDOMAIN NTLMServer yourDomainController NTLMBackup yourBackupDomainController Require valid-user </Location>
Instead of putting the above statements into a <Location>
section,
you can also put them into an existing <Directory>
section.
Point your browser to /ntlmtest
on your server (http://yourserver/ntlmtest
)
or to the directory to which you added the above http.conf statements. If you get
a password prompt or an “Authentication Required” error page, something
isn’t working. If you get an “Object Not Found” 404-error page,
that’s good. Note that you must use Mozilla Firefox 1.0 for Windows or Internet
Explorer when you test this and that you have to be logged on as a domain user.
Firefoxies, read this: In order to use NTLM auth on direct HTTP connections (as
opposed to proxy connections that support NTLM out of the box) you need to add
your server to the list of trusted URIs. To do so, start Firefox, type about:config
into
the address text field, find the setting network.automatic-ntlm-auth.trusted-uris
,
double-click it and enter http://yourserver
(no slash at the end).
This might seem tedious but for security reasons it’s a good idea to limit
the servers that a user agent exchanges NTLM challenges with because NTLM auth
is susceptible to man-in-the-middle attacks. Better safe then sorry. If you want
to be sorry, you can set the trusted URIs setting to http://
(two
slashes at the end).
Also, check your server access logs (usually somewhere in /var/log
)
and see whether your test requests were logged with your user name. If not, something
is not working properly or your log format is non-standard (unlikely). You need
to fix this before you continue.
If you haven’t done so yet, you should install Zope and Plone now and setup up your Plone site. Don’t add any users yet. When install a Zope instance, you need to specify the credentials of the instance manager. The manager should be the only user available in Zope and Plone.
Step 2 – Zope/Plone and Apache
OK, we got NTLM authentication working and we have a functioning Plone and Zope installation. What’s left is chaining Apache and Zope such that
- Apache forwards the requests to Zope,
- Apache passes the authenticated user’s name to Zope and
- Plone accepts that user as a member of the Plone site.
There are plenty of ways to run Zope behind Apache:
- CGI -- For every request, Apache runs the python binary, passing the request headers in environment variables and the request body in a pipe (I think). The Zope response is sent back through a pipe. After this, the Zope process finishes. Extremely slow and not recommended.
- FastCGI – Apache creates a socket connection to an already running Zope and passes the request info through that connection. Zope handles the request and sends the response back through that connection. The connection is closed and re-opened for every request but the Zope process continues to run. Much faster. Additional environment variables can be transferred through the socket connection.
- mod_proxy alone – This is conceptually similar but technically different from FastCGI. Apache works as a proxy and forwards the HTTP requests to Zope. This method is as fast as FastCGI. No environment variables can be passed along with the forwarded request. The URLs that Apache receives have the same postfix as the ones that Apache sends to Zope but the prefix can be different.
- mod_rewrite together with mod_proxy – Very similar to the previous solution but it allows for rewriting the URLs.
Although all of the above techniques have pros and cons, there is one killer requirement that rules out all but one technique: transferring the authenticated user from Apache over to Zope. So before we investigate how we are going to connect Apache and Zope, let’s have a look at the Zope authentication and permission system.
Disclaimer: I am more or less a Zope newbie so from here on out we will be moving into unknown territory. Don’t take everything I say for granted but don’t lose your faith either – the solution described in this article does actually work!
Zope (and hence Plone) organizes users in special folders called user folders. When added to an object, the user folder determines the available set of users for this object. Generally speaking, if a user folder is added to the Plone object, only the users in that user folder can access the Plone site. Well, not quite. Firstly, the Plone site can also be accessed by users of user folders further up in the hierarchical Zope database. For example, the users in the acl_users object at the Root Folder are granted access to the Plone site as well. Secondly, the users can be assigned Roles that further restrict the operations available to the users.
Different authentication mechanisms are added to Zope by adding specialized types
of user folders to a Zope instance. The default user folder is acl_users at the
Root Folder of any vanilla Zope instance. Another type of user folder is LDAP user folder
.
This user folder type doesn’t store the users in the Zope database but in
an LDAP directory such that LDAP user entries appear as Zope users. I also want
to mention GRUF (Group User Folder) as it is a very common user folder for Plone
sites. (See Appendix 5 – What about Group User Folder? )
Another type of user folder, the so-called Remote
User Folder is of particular interest to us. It doesn’t contain any
persistent user objects at all. Instead, it assumes that authentication has already
been completed for an incoming HTTP request and it looks up the request’s
authenticated user in its own list of users. A Remote User Folder obtains the
name of the authenticated user from the REMOTE_USER
environment
variable. As mentioned earlier, environment variables are not part of a standard
HTTP request. For this reason we cannot use either of the two mod_proxy solutions
because mod_proxy uses HTTP only. We’ll have to stick to either CGI or
FastCGI.
If the user isn’t present, a Remote User Folder will either reject the user (this is the default setting) or create a matching user entry automatically, provided that this feature is enabled. The on-the-fly creation of users is particularly helpful for our purposes, as it completely delegates authentication and the user management to some other instance. Finally, we have all the ingredients:
- The browser sends a request to the Apache web-server together with an invitation for NTLM authentication of the currently logged on user
- Apache receives the request
- The ntlm_auth module authenticates the request by mediating the NTLM challenge/response handshake between the user’s browser and the domain controller
- Once the user is authenticated, Apache dispatches the request to the Zope server, using the FastCGI protocol and including the authenticated user’s name in the REMOTE_USER environment variable
- Zope receives the request and - based on the path of the requested object – passes the request to the appropriate user folders for authorization
- The Remote User Folder obtains the user’s name from
REMOTE_USER
, scans its user list for that user name and adds an entry for the user if it is not yet present. Depending on the type of the requested object and the roles assigned to the user, the request is either denied with a 401 response or granted and executed.
Although this all sounds very lengthy, it is in fact rather easy to achieve.
Step 3 – FastCGI
Download mod_fastcgi package from the FastCGI homepage and follow the instructions in the INSTALL file (or INSTALL.AP2 for Apache 2). Compiling and installing FastCGI was a bit of headache for me because FastCGI uses Apache’s build system and I didn’t have a proper set of Apache headers and makefiles. My Apache installation is a backport RPM for my ancient Suse 7.1 so I think my difficulties are more the packager’s fault than FastCGI’s.
LoadModule fastcgi_module /usr/lib/apache2-prefork/mod_fastcgi.so
FastCgiExternalServer /usr/local/httpd/htdocs/zope -host localhost:8081 -pass-header Authorization
<Location /zope> SetHandler fastcgi-script </Location>
The FastCgiExternalServer
statement tells FastCGI which request paths
should be handled by mod_fastcgi. I’m not sure why it’s specified as
a file system path instead of a URL path but the file name is just a dummy and
the file does not need to exist. You can choose any other name instead of zope
but if you do that, you’ll need to adjust the following instructions accordingly.
The Location
statement tells Apache that requests to /zope
are
handled by FastCGI.
The -pass-header
option tells Apache to send authorization information
along with the requests. This doesn’t affect NTLM authentication because
environment variable REMOTE_USER
will be set regardless of this option.
It only affects whether standard HTTP authentication headers will be handed over
to Zope, thus enabling us to use the Zope management interface (ZMI) through Apache
(see Appendix 1 – Exempting the ZMI from NTLM auth and Appendix
2 – Schizophrenia).
The -host
option denotes the host name and port number to which FastCGI
request will be sent. We need to enable Zope’s FastCGI server and configure
it to listen on the given port. Add the following lines to the zope.conf
configuration
file which is usually located in the Zope instance’s /etc
directory.
<fast-cgi>
address localhost:8081
</fast-cgi>
Restart Apache and the Zope instance.
Step 4 – Enable NTLM authentication for requests to Zope
You already tested NTLM auth. in Apache by adding a dummy /ntlmtest
location
section to Apache’s configuration file. Edit http.conf one more time and
modify the <Location /ntlmtest>
section we added earlier:
<Location /zope/intranet>
AuthType NTLM NTLMAuth on NTLMAuthoritative on NTLMDomain YOURDOMAIN NTLMServer yourDomainController NTLMBackup yourBackupDomainController Require valid-user </Location>
It is important that the intranet component of the location path matches the identity of the Plone site object. You can verify the identity using the ZMI.
Also note that we now have two Location sections: one with FastCGI for /zope
and
one with NTLM for /zope/intranet
. Because /zope
is a
prefix of /zope/intranet
, FastCGI and NTLM auth will
be active for /zope/intranet
. (For an explanation of why we do not
use NTLM auth for /zope
see Appendix 1 – Exempting
the ZMI from NTLM auth)
Restart Apache. For the remainder of this article I will assume that you know the basics about ZMI, like creating and deleting objects or changing object properties. (In case you don’t, there is plenty of information about ZMI on the web.)
Step 5 – Install Remote User Folder
- Download and install the Remote User Folder product in your Zope instance directory.
- Point your browser to the ZMI and log in as manager. Use Zope’s HTTP port (default is 8080) because we are not quite ready to connect via Apache/FastCGI.
- Remove the existing user folder from the Plone site object.
- Add a Remote User Folder to the Plone site object. It will be called
acl_users
. Make sure to add it to the Plone site, not theRoot Folder
. - Enable automatic creation of user objects in Plone. This option is called
Auto Add Users
and can be found on the property tab of theacl_users
folder that we just created.
Appendix 1 – Exempting the ZMI from NTLM authentication
In Step 3 – FastCGI ) and Step 4 – Enable NTLM authentication for
requests to Zope ) we used distinct location sections, one for FastCGI and one
for NTLM auth. We did this for one main reason: Although Apache wraps the entire
Zope instance, NTLM auth should only be performed for URLs that point to the Plone
site. It’s important that /zope
and especially /zope/manage
are
exempt from NTLM auth because we do not want to lock out the Zope manager. The
Zope manager should be able to login using standard HTTP authentication. Furthermore,
there might be other Zope objects beside your Plone site for which NTLM auth would
not be appropriate.
Theoretically speaking, it should be possible to add the Remote User Folder to
the Zope instance root instead of the Plone site and to enable NTLM auth for the
entire Zope instance but there’s a caveat. I wasn’t able to replace
the default acl_users
folder with a different one, say Remote User
Folder (RUF) because after removing the acl_users
folder, I couldn’t
do anything at all because the manager (me) was locked out. There may be a trick
to get around this, though.
Appendix 2 – Schizophrenia
When accessing the ZMI through Apache/FastCGI, you will notice that you’ll
still have to enter the credentials of the manager. When you click on the intranet
icon representing the Plone site in the tree view on the left hand side frame,
you may notice that you turn from Zope manager into the Plone user that corresponds
with your Windows domain account. This is because the Plone part of the ZMI also
uses the /zope/intranet
URL prefix for which NTLM authentication is
enabled in Apache. For requests to this URL the browser sends NTLM authentication
headers instead of HTTP authentication. I’m not quite sure as to why NTLM
authentication overrides HTTP authentication, as there clearly is a conflict between
the two. If your domain account is not assigned the Manager role in Plone, you
will not get the management view of the Plone folder. Instead you will get the
Plone home page. In order to fix this you need to connect to the ZMI directly via
Zope’s HTTP port and assign the Manager role to the user object in Plone’s acl_users
folder
that corresponds to your Windows domain account.
Appendix 3 – Disabling Zope’s http server
Unless you already disabled the HTTP server in zope.conf, your Zope instance is listening on both the FastCGI and the HTTP ports. Using HTTP you should still be able to access everything in Zope including the management interface and excluding the Plone site. But because a Remote User Folder depends on the REMOTE_USER environment variable, access to the Plone site is only possible via FastCGI and thus through Apache. I recommend disabling the HTTP port in zope.conf because we don’t need it. The entire Zope site is accessible through Apache and FastCGI. Because HTTP is a more direct and robust way of connecting to your Zope instance, it might be necessary to temporarily enable the HTTP port for maintenance and debugging.
Appendix 4 – Virtual Host Monster (VHM) …
… is not needed apparently. VHM or SiteRoot objects are used whenever Zope runs behind something that disguises Zope’s internal URLs. VHM and SiteRoot make sure that URLs generated by Zope are mapped to the external URL format. Although this also applies to the solution described here, it seems that the URL mapping is taken care of by Zope’s FastCGI server.
Appendix 5 – What about Group User Folder?
Group User Folder (GRUF) can store users and groups. Instead of maintaining its own list of users and groups, it delegates the user and group storage to any number of user folders of any type. In that respect it is some kind of compound user folder, because it contains other user folders. The contained user folders are called sources. At the time of writing, Remote User Folder and GRUF are not compatible, i.e. Remote User Folder cannot be used as a source for GRUF. Stay tuned for updates on this issue …
Update, July 28th, 2005: Ok, you kept coming back, so here is an update ;-)
Apparently, GRUF doesn't implement validate() and thus never gives RemoteUserFolder any chance to do its work. This patch fixes GRUF when RemoteUserFolder is used as a source to a Group User Folder. The patch implements validate() in a way that I think should work in other cases as well. But who am I to promise you anything? Being a total Zope noob, I learned about Zope's acquisition today and am still gasping ...
Click here to download GroupUserFolder.py.patch
Apply the patch by doing
cd <your plone installation>/Products/GroupUserFolder wget http://www.hannesschmidt.de/files/GroupUserFolder.py.patch patch -b < GroupUserFolder.py.patch zopectl restart
Click here to download GroupUserFolder.py-3.3.patch, which does the same thing to the new GRUF 3.3
Update, Oct 1st, 2005: Did more work to RUF:
I think there were a lot of things wrong with RUF. At least there was one obvious bug (RUF not assigning roles automatically from DOMAIN\*) and some things going on for which I saw no reason at all. So I went ahead, fixed the bug and removed the code that seemed unnecessary. Download my new version (see list of attachments below) and replace your existing RemoteUserFolder.py. With the patches to GRUF-3.3 and the fixed RUF installed, I have a perfectly working portal in production now. If you have done testing with previous versions of my patches, you better make sure that you delete any existing GRUF and RUF instances and Members before you apply the patches. Afterwards create vanilla RUF and GRUF instances.
Update, Oct 21st, 2005: RemoteUserFolder v0.5:
This version has a workaround for an error reported by jmarks. Searching for users in the sharing tab produces a SiteError when the wildcard user "*" is present in RUF. I could not find the cause for this but I was able to provide a workaround by overriding RUF's getUsers() to simply not return the wildcard user. This means that RUF must be used to assign roles to the wildcard user. For regular users both RUF and GRUF may be used.
Update, Nov 8th, 2005: RemoteUserFolder v0.5.1:
With the help of Joey (jmarks), I fixed two more bugs, an obvious one in the Kerberos/NTLM functionality I added to v0.5 and a not so obvious one. According to ZODB documentation, it is not possible to pickle objects with acquisition wrapper. Part of my first modifications to RUF was a refactoring of __of__( self ) into _doAddUser(). Unfortunately, that wasn't a good idea. _doAddUser should really just return a plain RemoteUser instance. GRUF on the other hand needs a wrapped instance of RemoteUser, such that it can get to the user folder that created the RemoteUser instance. So with this version I refactored __of__( self ) calls back to validate().
Appendix 6 - Problems with mod_ntlm under load
People have reported that repeatedly refreshing the page causes a basic authentication dialog to appear. I initially though that it was a browser problem so I tested both Firefox and Internet Explorer. I was able to reproduce that problem under both browsers. The forum and bugtracking area sourceforge project page contains numerous posts describing the same symptoms, so the problem must be in mod_ntlm. In this post, Michael Cai and Jamie Kerwick announce their own improved version of mod_ntlm and mod_ntlm2 [8], which supposedly fixes this and other issues. I tried it out and was not able to reproduce the problem anymore. The bottom line is that you shouldn't use the official version. Instead, use Michael Cai's unofficial MOD_NTLM Apache module.
Sidenote: The mod_ntlm project managers seems to have abondened their baby. The last sign of life from them was spotted in May 2004, announcing a new version to be due "in a few days". Since then, various people have attempted to fix/rewrite mod_ntlm (see the project forum [3]).
Appendix 7 - Multiple Windows Domains
The mod_ntlm version described in Appendix 6 has support for multiple domains. If anybody has tested this feature, I'd appreciate it if they could contact me.
Update, Nov 8th, 2005: Patch to Michael Cai's mod_ntlm
Apparently, Michael Cai's mod_ntlm authenticates against multiple domains but it doesn't pass the authenticated user's domain name to Apache. This means that further down the line, RUF and GRUF won't be able to distinguish users from multiple domains. I modified Michael's version of mod_ntlm (because that's the only one that works reliably) such that it passes the user information in the form DOMAIN\User. You'll find the patch for Apache 2 in the attachements section at the end of this article. Let me know if you need one for Apache 1.3. I don't have 1.3 so I have no way of testing it.
Note that you shouldn't enable RUF's Simple Usernames property if you use this patch in conjunction with GRUF. This is because GRUF is confused about user names and id's.
Appendix 8 - Kerberos
Andrew Bartlett has rewritten mod_ntlm_winbind and he claims that it's coded more cleanly than mod_ntlm and that
"It is also a very good base to add a SPENGO (Negotiate) module, that accepts NTLMSSP as well as kerberos".
There is no official homepage and it may only work with Samba3. You're on your own. Let me know if you succeed.
Update, Oct 21st, 2005: mod_auth_kerb:
Our user jmarks reportedly got this to work with mod_auth_kerb by tweaking RUF to use the User@Domain syntax instead of Domain\User. He posted his version of RUF in a comment to this article. Today I updated RUF such that it can be switched dynamically between the two syntaxes. This updated RUF can now be used with both mod_ntlm and mod_auth_kerb.
Resources
- Zope.org
- Plone.org
- mod_ntlm Sourceforge project page
- mod_ntlm project home page
- FastCGI.com - The FastCGI home page
- Remote User Folder
- Group User Folder (GRUF)
- Unofficial MOD_NTLM Apache Module
- mod_ntlm_winbind back alive
Attachment | Size |
---|---|
GroupUserFolder.py-3.3.patch.bz2 | 697 bytes |
GroupUserFolder.py.patch.bz2 | 784 bytes |
mod_ntlm.c-apache2.patch.bz2 | 547 bytes |
RemoteUserFolder-0.5.1.tar.bz2 | 7.29 KB |