IMP & Zope integration HOWTO
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
participants (1)
-
Milos Prudek