[Zope] IMP & Zope integration HOWTO

Milos Prudek milos.prudek@tiscali.cz
Tue, 22 Jan 2002 19:38:51 +0100


IMP integration with Zope HOWTO
Version 0.8
January 2002

INTRODUCTION:
=============
IMP is a popular and powerful webmail system available at no cost from
http://www.horde.org. IMP is written in PHP.

Zope is a web application framework. A decent webmail system has not yet
been written for Zope.

INGREDIENTS:
============
- Zope (tested with Zope 2.4.3)
- SQL database (tested with PostgreSQL 7.1.3)
- IMP (tested with IMP 3.0 / Horde 2.0)
- IMAP server (required by IMP; you may be fine with just a POP3 server,
but I did not test that yet)
- exUserFolder (abbreviated as XUF) (tested with 0.9.0)

RESULT:
=======
At the of this tutorial, you will be able to sign-in to IMP any user
that is currently authenticated in Zope. The user will not need to
re-authenticate manually against IMP.

STEPS:
======

Install and test XUF. Make sure that your imap server accepts users and
passwords defined in XUF.

For this example it is sufficient to instantiate XUF with Null property
source, Null membership source, and PostgreSQL authentication source.

This HOWTO assumes that you instantiated XUF in folder "Folder1". In
real world you will need to instantiate XUF in all folders that you need
SQL authentication, since root installation of XUF is not recommended.

This HOWTO assumes that your Zope is behind Apache, and that it is
accessed as http://localhost/Zope. Your IMP should be accessed as
http://localhost/horde/imp.

NOTE: Zope is behind Apache because otherwise IMP would not be able to
get a cookie from ":8080" host, because IMP will be at port 80.

For XUF, you will need to create in PostgreSQL a table named "passwd"
with at least the following columns:
username        varchar(64)
password        varchar(64)
roles           varchar(255)
password_plain  text
Please read further info in XUF documentation. The "password_plain"
column is neccessary for this HOWTO. Since you will store plaintext
passwords there, make sure that PostgreSQL is well secured.

For Zope/IMP integration, you will need to create in PostgreSQL a table
named "zopeimp" with the following columns:
taq             text
username        text
password        text


Now within "Folder1" place the following DTML Methods, Python Scripts
and ZSQL Methods:


PYTHON SCRIPT "set_cookie":
from Products.PythonScripts.standard import html_quote
request = container.REQUEST
RESPONSE =  request.RESPONSE
luser=ns.SecurityGetUser().getUserName()
password_plain=context.get_password_plain(username=luser)[0].password_plain
tag1=request.REMOTE_ADDR
tag2=DateTime().timeTime()
tag=tag1+"-"+repr(tag2)
context.insert_tag(tag=tag,username=luser,password=password_plain)
RESPONSE.setCookie('zopeimp',tag,path='/')
return


ZSQL METHOD "get_password_plain" (parameter is username):
SELECT password_plain FROM passwd
WHERE <dtml-sqltest username op=eq type=string>;


ZSQL METHOD "insert_tag" (parameters are tag, username, password):
insert into zopeimp values(
  <dtml-sqlvar tag type=string>,
  <dtml-sqlvar username type=string>,
  <dtml-sqlvar password type=string>);

DTML METHOD "implogin":
<dtml-call "set_cookie()">
<dtml-call
"RESPONSE.redirect('http://localhost/horde/imp/myredirect.php')">


Within your filesystem you will need the ../horde/imp/myredirect.php. It
is a copy of IMP's original redirect.php file, with a simple patch.

Here's the myredirect.php file:
<?php
/*
 * $Horde: imp/redirect.php,v 1.23.2.3 2002/01/02 17:05:32 jan Exp $
 *
 * Copyright 1999-2002 Charles J. Hagenbuch <chuck@horde.org>
 * Copyright 1999-2002 Jon Parise <jon@horde.org>
 *
 * See the enclosed file COPYING for license information (GPL).  If you
 * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
 */

define('IMP_BASE', dirname(__FILE__));
require_once IMP_BASE . '/lib/base.php';

// PATCH TO INTEGRATE ZOPE AND IMP STARTS HERE
if (!isset($HTTP_COOKIE_VARS["zopeimp"])) {
   echo "<h2>Your browser did not give neccessary cookie</h2>";
} else {
   $zi_cookie=$HTTP_COOKIE_VARS["zopeimp"];
   $zi_tag=substr($zi_cookie,1,strlen($zi_cookie)-2);
   $zi_tag="'" . $zi_tag . "'";
   // BE SURE TO CUSTOMIZE THE FOLLOWING LINE:
   $zi_c=pg_Connect("host=localhost port=5432 dbname=maindb user=john
password=secret");
   $zi_r=pg_exec($zi_c,"SELECT username,password FROM zopeimp WHERE
tag=$zi_tag;");
   $zi_r2=pg_exec($zi_c,"DELETE FROM zopeimp WHERE tag=$zi_tag;");
   $zi_num=pg_numrows($zi_r);
   if ($zi_num==1) {
      $HTTP_POST_VARS['imapuser']=pg_result($zi_r,0,"username");
      $HTTP_POST_VARS['pass']=pg_result($zi_r,0,"password");
      // BE SURE TO CUSTOMIZE POST VARS TO YOUR NEEDS!
      $HTTP_POST_VARS['server'] = 'localhost';
      $HTTP_POST_VARS['actionID'] = '105';
      $HTTP_POST_VARS['mailbox'] = 'INBOX';
      $HTTP_POST_VARS['port'] = '143';
      $HTTP_POST_VARS['maildomain'] = 'domain.com';
      $HTTP_POST_VARS['protocol'] = 'imap';
      $HTTP_POST_VARS['realm'] = 'domain.com';
      $HTTP_POST_VARS['folders'] = 'mail%2F';
      $HTTP_POST_VARS['new_lang'] = 'cz_CZ';
      $HTTP_POST_VARS['button'] = 'P%F8ihl%E1%B9en%ED+do+syst%E9mu';
   } else {
      if ($zi_num==0) 
      {
         echo "<h2>Your browser sent a cookie we do not have on
file!</h2>";
      } else {
         echo "<h2>Your browser sent a cookie that we have filed more
than once!</h2>";
      }
   }
}
// PATCH TO INTEGRATE ZOPE AND IMP ENDS HERE

$action = Horde::getFormData('action', '');
if ($action === 'compose') {
    $actionID = LOGIN_COMPOSE;
} else {
    $actionID = Horde::getFormData('actionID', IMP_LOGIN);
}

/* If we already have a session... */
if (isset($HTTP_SESSION_VARS['imp']) &&
is_array($HTTP_SESSION_VARS['imp'])) {
    /* Make sure that if a username was specified, it is the current
username */
    if ((!isset($HTTP_POST_VARS['imapuser']) ||
$HTTP_POST_VARS['imapuser'] == $HTTP_SESSION_VARS['imp']['user']) &&
        (!isset($HTTP_POST_VARS['pass']) || $HTTP_POST_VARS['pass'] ==
Secret::read(Secret::getKey('imp'), $HTTP_SESSION_VARS['imp']['pass'])))
{
        
        if ($actionID == IMP_LOGIN) {
            $actionID = NO_ACTION;
        }
        header('Location: ' .
Horde::applicationUrl('mailbox.php?actionID=' . $actionID, true));
        exit;
    } else {
        /* Disable the old session. */
        $imp = false;
        session_unregister('imp');
        header('Location: ' .
Horde::applicationUrl(IMP::logoutUrl('login.php', 'failed'), true));
        exit;
    }
}

/* Create a new session if we're given the proper parameters. */
if (isset($HTTP_POST_VARS['imapuser']) &&
isset($HTTP_POST_VARS['pass'])) {
    if (!isset($HTTP_POST_VARS['mailbox'])) {
        $HTTP_POST_VARS['mailbox'] = 'INBOX';
    }
    if (($reason = IMP::createSession()) === true) {
        $imp['_login'] = true;
        $entry = sprintf('Login success for %s [%s] to {%s:%s}',
                         $imp['user'], $HTTP_SERVER_VARS['REMOTE_ADDR'],
$imp['server'], $imp['port']);
        Horde::logMessage($entry, __FILE__, __LINE__, LOG_NOTICE);

        if (Horde::getFormData('redirect_url')) {
            header('Location: ' . Horde::getFormData('redirect_url'));
            exit;
        }

        header('Location: ' .
Horde::applicationUrl('mailbox.php?actionID=' . $actionID, true));
        exit;
    } else {
        header('Location: ' .
Horde::applicationUrl(IMP::logoutUrl('login.php', $reason), true));
        exit;
    }
}

/* No session, and no login attempt. Just go to the login page. */
$uri = 'login.php';
if (!empty($HTTP_SERVER_VARS['QUERY_STRING'])) {
    $uri .= '?' . $HTTP_SERVER_VARS['QUERY_STRING'];
}
header('Location: ' . Horde::applicationUrl($uri, true));
exit;

?>



Everything is in place now. 

Make sure that the "implogin" DTML Method has View and Access contents
information permissions accessible to Authenticated User.

Quit your browser. Access http://localhost/Zope/Folder1/implogin. XUF
will prompt you to enter your credentials. When you do that, you will be
automatically transported to IMP. That's the whole magic.



HOW IT WORKS:
=============

"implogin" calls "set_cookie()".

set_cookie reads plaintext password of the current user. Then set_cookie
invents a pretty unique tag. Then set_cookie inserts a new record in a
special "zopeimp" table. The record contains the tag, the username and
the plaintext password. The tag will be used by IMP to retrieve the
password. Therefore, set_cookie stores the tag in a cookie at the
client's browser.

Note:
This cookie expires at the end of browser session. A marginally cleaner
solution would be if IMP expired the cookie, but I found it non-trivial
to implement. Do it if you want, and send me the patch.

Note:
The record in "zopeimp" table is destroyed automatically by IMP. It
exists for a split of a second, and acts as a glue between Zope and IMP.

That's the end of "set_cookie()". We are back at "implogin".

Now "implogin" redirects to http://localhost/horde/imp/myredirect.php. 

Now we are at "myredirect.php".

"myredirect.php" will pick the tag from a cookie. It will remove double
qoutes and replace them with single quotes, because PostgreSQL does not
like double quotes in parameter value.

"myredirect.php" will now use the tag to retrieve username and password.
It will immediately destroy the record that it just retrieved. Then it
will fill in remaining values that are neccessary for successful IMP
sign-on.

"myredirect.php" contains some simple error checking.



RATIONALE:
==========

There are more methods possible to achieve the above-described effect. 

One method would be to use PHPDocument product. This product should
enable running PHP code from within Zope. Such solution would be much
simpler than the one presented here, because all cookie and SQL magic
would be unneccessary. Unfortunately, I was not able to achieve it, so I
gave up.

Another method would be to keep the plaintext password in a session,
such as CoreSessionTracking. The problem here is that you would need a
hook in XUF that would execute a "save to cookie" method when successful
login happens. It would be great if XUF included a hook to have
user-defined method run when successful login happens. I'm not that good
at hacking XUF, so I did not do it that way.

Yet another method would be to simply put plaintext password and
username into hidden HTML fields and POST it to normal redirect.php. The
dissadvantage here is that password would appear in any web cache on the
way, and it would be stored at local computer, too. This is so insecure
that I never considered such approach. However, this approach would be
completely secure if all traffic went through SSL. My goal was to do
without SSL and yet achieve some semblance of security.



SECURITY CONCERNS:
==================
An attacker sitting in between browser and server may intercept the
"implogin" request, capture the tag, cut the line to server so that the
browser never receives "redirect.php" from server, and present itself as
the browser using the tag.

However, the attacker that sits between browser and server does not need
to do such painful activity in order to read user's mail. He can simply
retrieve the password during form authentication, because HTTP is
unencrypted.

In other words, there are more serious issues for in-between person than
cookie hijacking. AFAIK there is only one way to stop these issues: use
SSL.



FINAL NOTES:
============
This HOWTO describes a fully working solution. The HOWTO is designated
version 0.8, because I'm not sure if I put all neccessary information
in.

Please send your comments to milos.prudek@tiscali.cz

--
Milos Prudek