\documentclass{article}
\usepackage{ulem}
\pagestyle{headings}
\begin{document}
\part{ CAS Authentication HowTo}
Jan Van der Velpen aka Velpi (who did all the work)\newline
   Peter Arien aka Kaos99 (who just likes playing around with Horde)\newline
  Thanks go to the Ja-Sig\footnote{http://www.ja-sig.org/cas/} and the ESUP\footnote{http://esup-portail.org/consortium/espace/SSO\_1B/tech/cas/cas\_install.html} people!!

Our university\footnote{http://www.kuleuven.be/} is working towards a complete AAI (Authentication and Authorization Infrastructure) implementation. For web applications we are using the Shibboleth architecture\footnote{http://shibboleth.internet2.edu/}. But as you can read in the Shibboleth Authentication HowTo\footnote{http://example.com/index.php?page=ShibbolethAuthHowTo},  a big problem with AAI and webapplications is authentication on the backend (with Horde/IMP that would be the mailservers). What we needed was a way to prevent the password passing the webmail servers AND the mailservers.

Meet CAS: "Central Authentication System". It was originally developed by Yale and then adopted by the JA-SIG group. The ESUP consortium is also actively developing in the CAS area.

We chose to use CAS (http://www.ja-sig.org/products/cas/index.html\footnote{http://www.ja-sig.org/products/cas/index.html}) as an authentication mechanism on top of Shibboleth. Because both Shibboleth and CAS do the initial authentication at the CAS server, users will see it as one integrated SSO system. Specific information about our implementation of CAS and Horde can be found at http://shib.kuleuven.be/docs/horde3-cas/\footnote{http://shib.kuleuven.be/docs/horde3-cas/}

First we used the ESUP pam module (referenced here\footnote{http://www.ja-sig.org/wiki/display/CAS/PAM+Module}) to let our mailservers use the CAS server as a possible authentication service. Here's how the cas lines in our mailserver pam-config looks like:\newline
\texttt{/etc/pam.conf:}

\begin{verbatim}

 imap    auth    sufficient      /usr/lib/security/pam_cas.so -simap://127.0.0.1 -f/etc/pam_cas.conf
 imap    auth    sufficient      /usr/lib/security/pam_ldap.so try_first_pass
\end{verbatim}

For a Debian+Dovecot-and-ldap machine the entire file could look like (/etc/pam.d/dovecot):\newline
[20081009 Added by Velpi]\newline
\begin{verbatim}

auth    sufficient      /lib/security/pam_cas.so -simap://127.0.0.1 -f/etc/pam_cas.conf
auth    sufficient      pam_ldap.so config=/etc/pam_ldap.conf
account required        pam_ldap.so config=/etc/pam_ldap.conf
session required        pam_ldap.so config=/etc/pam_ldap.conf
\end{verbatim}

\texttt{/etc/pam\_cas.conf:}

\begin{verbatim}

 host cas.example.com
 port 80
 uriValidate /cas/proxyValidate
 ssl off
 debug off
 proxy https://webmail.example.com/hordecas/casProxy.php
 trusted_ca /etc/pki/example.com.chain
\end{verbatim}

note that this configuration means we're validating the PT to our CAS server at port 80 (regular http), which isn't the best thing to do considering security, but it saves quite some CPU cycles.\newline
If you're not sure about the network between your IMAP and CAS server then certainly use SSL, port 443 and trusted\_ca!

Next step was to make the ESUP Horde CAS authentication driver work on our webmail servers using Horde 3.1.1 and IMP 4.1.2.

For now I'll just copy/paste Velpi's *notes*:

HOWTO CASify HORDE3 AND IMP4 [Velpi;20051201, Kaos99; 20060620, \ldots]\newline
\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#

Tested succesfully using standard Debian packages [20051206]

\begin{itemize}
\item{Horde 3.0.4-4}
\item{IMP 4.0.2-2}
\end{itemize}
Connection problems when using horde from CVS [framework\_3 20051220]\newline
Tested succesfully using standard horde release packages [20060620]

\begin{itemize}
\item{Horde 3.1.1}
\item{IMP 4.1.2}
\item{phpCAS 0.4.22-RC with patches (see below)}
\end{itemize}
Tested succesfully using standard horde release packages [20081009]

\begin{itemize}
\item{Horde 3.1.3}
\item{IMP 4.1.3}
\item{phpCAS 0.6.0}
\end{itemize}
First, install a basic horde system\newline
Configure it to use IMAP auth for horde-auth\newline
Set imp/conf/servers.php correctly for your backend and set 'hordeauth' => true\newline
You will need an IMAPPROXY to cache the connections when using CAS. It is a good habit to install it too when not using CAS.\newline
We use up-imapproxy from http://www.imapproxy.org/\footnote{http://www.imapproxy.org/} .

Check your current system so everything works at this point (DO IT!)\newline
Now we can start patching it to use CAS\newline
(if you didn't check your "normal" system at this point you will most likely curse if you need to debug, you have been warned\ldots)

1) configure Apache\newline
Apache HAS to be configured to use SSL for horde when using CAS. CAS relies on SSL to make sure it's talking to right server, that and encryption of course.\newline
PHP (curl) should trust the certificateS that will be offered by your CAS-server. This means you need to feed the certificate of the (root CA of the) CAS server to Apache in its trust directive.\newline
-----------httpd.conf------------\newline
SSLCertificateFile\footnote{http://example.com/index.php?page=SSLCertificateFile} /etc/pki/myHORDEserver.pem\newline
SSLCertificateChainFile\footnote{http://example.com/index.php?page=SSLCertificateChainFile} /etc/pki/ca\_cert.pem\newline
\#added for the trust mechanism----\newline
SSLCACertificateFile\footnote{http://example.com/index.php?page=SSLCACertificateFile} /etc/pki/ca\_cert.pem\newline
\#----added\newline

\noindent\rule{\textwidth}{1pt}
\newline
If you see an error in CAS logs about a missing PGTIou then you did this step wrong.[You may also consider downloading the Horde-CAS package from the ESUP consortium that does every one of the next steps automatically. It is located at http://www.esup-portail.org/consortium/espace/download/horde/\footnote{http://www.esup-portail.org/consortium/espace/download/horde/}]

2) install phpCAS library in horde\newline
phpCAS uses domxml for php4.3, php5 means phpCAS will use a conversion class automatically. The Auth driver for Horde checks whether all necessary components are installed.\newline
K.U.Leuven's Horde-CAS authentication driver is patched to use PHP5. This means the check for domxml is commented out.

phpCAS has become a JA-SIG project, see:\newline
http://www.ja-sig.org/wiki/display/CASC/phpCAS\footnote{http://www.ja-sig.org/wiki/display/CASC/phpCAS}\newline
(extract the package and)\newline
[change the path to your horde/php lib dir accordingly]\newline
phpCAS 0.6 and lower:\newline
\begin{verbatim}

mkdir $HORDE_DIR/lib/CAS/ 
cp -r $PHPCAS_SOURCE_DIR/CAS/* $HORDE_DIR/lib/CAS/
\end{verbatim}

\newline
phpCAS 1.0 and higher (DOES NOT WORK: K.U.Leuven driver needs adjustments!! To be continued\ldots; 20081009):\newline
\begin{verbatim}

mkdir $HORDE_DIR/lib/CAS/
cp -r $PHPCAS_SOURCE_DIR/CAS.php $HORDE_DIR/lib/CAS/
mkdir $HORDE_DIR/lib/CAS/CAS/
cp -r $PHPCAS_SOURCE_DIR/CAS/* $HORDE_DIR/lib/CAS/CAS/
\end{verbatim}

3) install horde driver and proxyticketReceptor script for phpCAS\newline
K.U.Leuven made these two files public available with some modifications.\newline
You can download them from http://shib.kuleuven.be/docs/horde3-cas/horde\_cas\_auth\_driver/\footnote{http://shib.kuleuven.be/docs/horde3-cas/horde\_cas\_auth\_driver/}\newline
[the CAS auth driver for horde]\newline
cp \$CAS\_DIR/cas.php \$HORDE\_DIR/lib/Horde/Auth/\newline
[the callback url for the PGT=proxyticketReceptor]\newline
cp \$CAS\_DIR/casProxy.php \$HORDE\_DIR/\newline
\textsl{--\newline
IMPORTANT NOTE:\newline
for CAS3: the regex matches to "PT" AND to "ST"\newline
CAS2: ST, PGT, PT\newline
CAS3: ST, TGT (PT's are now regular STs and PGTs are now TGTs)\newline
}--

4) set IMP to use horde credentials\newline
/imp/config/server.php\newline
hordeauth => true

5) patch IMP: IMP has to request a new PT if necessary (PT are only valid for ONE login at the IMAP)\newline
notes:

\begin{itemize}
\item{hordeauth=true => horde "pass" will be a PT that phpCAS has already requested}
\item{imapproxy HAS to be used, else IMP will need to detect that this PT is invalid for login and request a new one FOR EACH REQUEST! [imapproxy solves this problem smoothly, don't hesitate to use it; SASLauthd should solve this too if you are using a cyrus IMAP backend or so]}
\end{itemize}
\begin{verbatim}

diff -ru1b /usr/src/imp-h3-4.1.2/lib/Auth/imp.php imp/lib/Auth/imp.php
--- /usr/src/imp-h3-4.1.2/lib/Auth/imp.php      2006-04-10 07:03:44.000000000 +0200

+++  imp/lib/Auth/imp.php        2006-05-05 11:41:27.000000000 +0200

@@ -268,2 +268,11 @@

+        //VELPI--

+        $entry = sprintf('LOGIN OK %s to %s:%s[%s] as %s',

+                          $_SERVER['REMOTE_ADDR'],

+                         $_SESSION['imp']['server'],

+                          $_SESSION['imp']['port'],

+                         $_SESSION['imp']['protocol'],

+                          $_SESSION['imp']['user']);

+        Horde::logMessage($entry, __FILE__, __LINE__, PEAR_LOG_DEBUG);

+        //--VELPI

         return true;

diff -ru1b /usr/src/imp-h3-4.1.2/lib/IMAP.php imp/lib/IMAP.php
--- /usr/src/imp-h3-4.1.2/lib/IMAP.php  2006-04-10 07:03:44.000000000 +0200

+++  imp/lib/IMAP.php    2006-05-10 09:27:09.000000000 +0200

@@ -103,11 +103,58 @@
         }
-

+        /* CAS: [VELPI]

+         GrEaT: the new IMP version automaticaly retries => so we can just hop the wagon and get a new ticket on failure!

+          Login failure might mean bad password: always retry when using CAS because we'll request a new password if needed.

+         Do 4 attempts, assume current pasword should work most of the time (cache/proxy timeout should be large enough!):

+          1) current pwd

+         2) new ticket

+          3) retry with the same, new ticket (after short sleep)

+         4) another new ticket

+        */

+       //keep retry-ing connect: max 4 attemps; stop when "login failure" and not using CAS (CAS=request new pwd on fail)
         while (($ret === false) &&
-               !strstr(strtolower(imap_last_error()), 'login failure') &&
-               (++$i < 3)) {
-            if ($i != 0) {

+                ! (   (strstr( strtolower(imap_last_error()), 'login failure' ) )

+                       && ($GLOBALS['conf']['auth']['driver'] != "cas")   ) &&

+                (++$i < 4)) {

+            if ($i > 0) {

+                //every pass except the first

+               Horde::logMessage("short sleep hoping for delay fix on IMAP connect", __FILE__, __LINE__, LOG_INFO);
                 sleep(1);
-            }

+ 

+                       //-----CAS: get a new ticket if connection is lost-----

+                Horde::logMessage("login fail on pass [$i]", __FILE__, __LINE__, LOG_INFO);

+               //request new ticket on each second or fourth pass

+                if ( ($i==1 || $i==3) && $GLOBALS['conf']['auth']['driver'] == "cas") {

+                       Horde::logMessage("login fail user=".$this->_user."; serverString=".$this->_serverString."; requesting new ticket [pass $i]"

+                                        , __FILE__, __LINE__, LOG_WARN);

+                       $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']);

+                        if(is_a($auth,"Auth_composite")) {

+                               if (($login_driver = Auth::_getDriverByParam('loginscreen_switch', $auth->_params)) &&

+                                        $auth->_loadDriver($login_driver)) {

+                                       $this->_pass = $auth->_drivers[$login_driver]->getNewPT();

+                                        }

+                       }

+                                elseif(is_a($auth,"Auth_cas")) {

+                               $this->_pass = $auth->getNewPT();

+                                }

+                       Horde::logMessage('new proxy ticket='.$this->_pass.' for user='.$this->_user, __FILE__, __LINE__, LOG_DEBUG);

+                } //END if cas

+               //this isn't needed since cas.php solves that itself, so let's not do this (keep it here as a reference)

+                        //$_SESSION['imp']['pass'] = Secret::write(Secret::getKey('imp'), $this->_pass);

+               //-----CAS: END get a new ticket if connection lost-----

+ 

+           } //END if $i>0

+ 

+           Horde::logMessage("IMAP connect [$i]:".$this->_serverString.' || '. $mbox .' || '. $this->_user

+                                .' || '. $this->_pass.' || '. $flags, __FILE__, __LINE__, LOG_DEBUG);

+           //the actual login attempt:
             $ret = @imap_open($this->_serverString . $mbox, $this->_user, $this->_pass, $flags);
         }

+        if ($ret===false) {

+               //still failed, and we're not going to try again... so let's send out some info to the admin (=> read the logs plz)

+                $local_severity=LOG_INFO;

+               //if we're using CAS this is a severe error

+                if ($GLOBALS['conf']['auth']['driver'] == "cas") $local_severity=LOG_ERR;

+               Horde::logMessage('LOGIN FAILED to serverString='.$this->_serverString, __FILE__, __LINE__, $local_severity);

+        }

+       Horde::logMessage('openIMAPStream return value is: '.$ret, __FILE__, __LINE__, LOG_DEBUG);
         return $ret;
@@ -129,3 +176,5 @@
         if (empty($_SESSION['imp']['stream'])) {

+            Horde::logMessage('no stream in session; requesting new IMAP stream', __FILE__, __LINE__, LOG_DEBUG);

             if (($_SESSION['imp']['stream'] = $this->openIMAPStream($mbox, $flags))) {

+                Horde::logMessage('new stream opened for mbox='.$mbox, __FILE__, __LINE__, LOG_DEBUG);

                 $this->_openMbox = $mbox;
@@ -135,3 +184,2 @@
                 }
-
                 if (!empty($_SESSION['imp']['imap_server']['timeout'])) {
@@ -154,2 +202,3 @@
         if (($this->_openMbox != $mbox) || ($this->_mboxFlags != $flags)) {

+            Horde::logMessage('imap_reopen: changing to mbox='.$mbox, __FILE__, __LINE__, LOG_DEBUG);

             $result = @imap_reopen($_SESSION['imp']['stream'], $this->_serverString . $mbox, $flags);

diff -ru1b /usr/src/imp-h3-4.1.2/lib/Session.php imp/lib/Session.php
--- /usr/src/imp-h3-4.1.2/lib/Session.php       2006-05-10 00:05:40.000000000 +0200

+++  imp/lib/Session.php 2006-06-20 10:25:54.000000000 +0200

@@ -205,3 +205,2 @@
         $_SESSION['imp']['mailbox'] = $_SESSION['imp']['thismailbox'] = '';
-
         /* Try to authenticate with the given information. */
@@ -261,2 +260,3 @@
                              * (per RFC 3501 [6.3.8]). */

+ 

                             $box = @imap_getmailboxes($_SESSION['imp']['stream'], IMP::serverString(), $val);
@@ -274,2 +274,3 @@
                     /* Auto-detect namespace parameters from IMAP server. */

+ /* VELPI: auto-detect fails with CAS&imapproxy => don't use

                     $res = $imapclient->login($_SESSION['imp']['user'], $password);
@@ -287,7 +288,10 @@
                     $_SESSION['imp']['imap_server']['children'] = $imapclient->queryCapability('CHILDREN');
-

+ */

                     /* Determine if the search command supports the current
                      * browser's charset. */

+ /*

                     $charset = NLS::getCharset();
                     $_SESSION['imp']['imap_server']['search_charset'] = array($charset => $imapclient->searchCharset($charset));

+                     $imapclient->logout();

+*/
\end{verbatim}

6) configure horde to use CAS\newline
note: don't forget to tell IMP to try hordeauth (imp/config/servers.php)\newline
you might want to use the built-in administration tools, but real men do it with vi ;)\newline
enabling CAS is easy now, just tell horde to use it:\newline
--------horde/config/conf.php---------- [part of! replace the auth thingies with something like this]\newline
\begin{verbatim}

//make sure horde won't put the CAS login screen in a frame, this will seriously mess up the browser window :(
$conf['menu']['always'] = false

//please make me admin
$conf['auth']['admins'] = array('u0049919');
...
//checkip is nice, but not when you're using NAT so turn it off :s
$conf['auth']['checkip'] = false;
...
//host name of your CAS server
$conf['auth']['params']['hostspec'] = 'myCASserver';
//most likely 443
$conf['auth']['params']['hostport'] = 443;
//the part that comes after the hostname eg 'cas' in https://myCASserver/cas
$conf['auth']['params']['hostpath'] = 'cas';
//the script that will receive PT's (part of phpCAS)
$conf['auth']['params']['proxyback'] = 'https://thisHORDEserver/horde/casProxy.php';
//PT's can be saved in a database too if you like; but a writable dir is fine
//note: should be writable by user that runs PHP/horde
$conf['auth']['params']['tmpdir'] = '/tmp';

//hooks into horde's as an ACL check (eg to LDAP); see hooks.php
$conf['auth']['params']['authorisation'] = false;

//you will need to see some logs at first to check everything, fairly verbose though
$conf['auth']['params']['debug'] = true;
//note: should be writable by user that runs PHP/horde
$conf['auth']['params']['debug_file'] = '/tmp/hordeaai-cas.log';

//yup, we're using cas now
$conf['auth']['driver'] = 'cas';

...
$conf['log']['name'] = '/tmp/hordeaai.log';
\end{verbatim}

\newline

\noindent\rule{\textwidth}{1pt}
\newline
Please note that CAS will request a PT for the service that it is trying to connect to.\newline
This means that the IMAP server that checks the PT needs to do that with the same service name as the ticket was requested for!\newline
(when using an IMAPPROXY -which you should- the service name will be "imap://127.0.0.1" or "imap://localhost")7) patch horde configuration interface\newline
notes:

\begin{itemize}
\item{horde uid (login name) will be the CAS netId when authenticated}
\item{CAS does no authorisation, everybody that can login to CAS, can enter horde (if no extra measures are taken, see next topic)}
\end{itemize}
enable configuration settings for horde auth:\newline
---------horde/config/conf.xml---------\newline
\begin{verbatim}

@@ -132,6 +132,19 @@
      </configdescription>
     </case>

+     <case name="cas" desc="CAS authentication">

+     <configsection name="params">

+       <configstring name="hostspec" desc="The hostname of the CAS server">cas.kuleuven.be</configstring>

+      <configinteger name="hostport" desc="The HTTPS Port of the CAS server">443</configinteger>

+       <configstring name="hostpath" desc="The root web path of the CAS server" required="false">cas</configstring>

+      <configstring name="proxyback" desc="The proxy URL of horde">https://webmail.kuleuven.be/horde3/casProxy.php</configstring>

+       <configstring name="tmpdir" desc="Temporary">/tmp</configstring>

+      <configboolean name="authorisation" desc="Use hook for authorisation (function _cas_hook_authorisation)">false</configboolean>

+       <configboolean name="debug" desc="Debugging">false</configboolean>

+      <configstring name="debug_file" desc="Debugging file">/tmp/phpCAS.log</configstring>

+      </configsection>

+    </case>

+ 

     <case name="ftp" desc="FTP authentication">
       <configsection name="params">
          <configstring name="hostspec" desc="The hostname or IP address of the FTP
\end{verbatim}

\newline

\noindent\rule{\textwidth}{1pt}
8) patch horde's hooks if you want authorisation (=check user with another backend)\newline
note: this has nothing to do with AUTHENTICATION! Meaning you don't need this to get CAS working.\newline
note: this is a configurable option (horde config.php: \$conf['auth']['params']['authorisation'])\newline
don't forget to configure this correctly if you want to use it (eg LDAP settings)\newline
---------horde/config/hooks.php---------\newline
\begin{verbatim}

if (!function_exists('_cas_hook_authorisation')) {
    function _cas_hook_authorisation($username = null)
    {
        if(empty($username)) {
            return(false);
        }

        $ldapServer = '__LDAP_HOST__';
        $ldapPort = '__LDAP_PORT__';
        $searchBase = '__LDAP_BASEDN__';
        $filter = "(&(uid=%s)(objectclass=eduPerson)(mail=*))";

        if(! $ds = @ldap_connect($ldapServer, $ldapPort)){
            return(false);
        }

        $filter = sprintf($filter,$username );

        $searchResult = ldap_search($ds, $searchBase, $filter,array('uid'));

        $information = @ldap_get_entries($ds, $searchResult);
        @ldap_free_result($searchResult);
        @ldap_close($ds);

        if(!is_array($information) || $information['count']!=1)    return(false);
     return(true);
     }
}
\end{verbatim}

\newline

\noindent\rule{\textwidth}{1pt}
[optional steps]

*) redirect on logout (highly recommended)\newline
Logging out is a little less easy when using a WebISO\footnote{http://example.com/index.php?page=WebISO} since it will automatically re-login when there is still a session with the central server.\newline
A simple workaround is to make the redirect on logout link to a location that doesn't need authentication.\newline
----horde/config/conf.php----\newline
\begin{verbatim}

...
$conf['auth']['redirect_on_logout'] = 'http://cas.example.be/cas/logout';
// or $conf['auth']['redirect_on_logout'] = 'https://idp.example.be/shibboleth-idp/logout.jsp?return=http://webmail.example.be';
...
\end{verbatim}

\newline

\noindent\rule{\textwidth}{1pt}
*) adjust the standard login page (recommended)\newline
You might want to adjust this page so it doesn't show a login box when using CAS.\newline
----horde/config/conf.php----\newline
\begin{verbatim}

//redirect back to IMP to make sure there's no frame-in-frame when sth goes wrong
$conf['auth']['alternate_login'] = 'https://cas.example.be/cas/login?service=https://'.$_SERVER['SERVER_NAME'].'/horde/imp';
\end{verbatim}

\newline

\noindent\rule{\textwidth}{1pt}
--INSTALL COMPLETED--

*) contributed by Maja Gorecka-Wolniewicz, Uczelniane Centrum Informatyczne:\newline
When a IMAP server is using non-standard port the CAS auth driver keeps\newline
asking for ticket for service imap://name while the ticket for\newline
imap://name:port is needed.\newline
I've added in function \_\_getIMPVars() after

\begin{verbatim}

 $this->_imapService = $p."://".$servers[$server]['server'];
\end{verbatim}

the code

\begin{verbatim}

if ( $servers[$server]['port'] != 143 ) $this->_imapService .=":".$servers[$server]['port'];
\end{verbatim}

Now it's time for debugging fun!\newline
try checking your email and keep an eye on these files:

\begin{itemize}
\item{at horde server: logfile of CAS that you specified (needs to be writable by user that runs PHP/horde), possibly apache on SSL errors}
\item{you might want to check imapproxy logs (also see "pimpstat")}
\item{at IMAP: /var/log/auth.log and /var/log/syslog}
\end{itemize}
DEBUG HINTS:

\begin{itemize}
\item{horde uid (login name) will be the CAS netId when authenticated}
\item{HORDE: see /tmp/hordeaai-cas.log (when debug=true and configured like in this document; needs to be writable by user that runs PHP/horde; possibly see apache on SSL errors)}
\end{itemize}
=> if contains "domxml\_open\_mem failed": the response from CAS server is not XML: use your browser to go to the URL that phpCAS shows in the logs right above the error

\begin{itemize}
\item{IMAP proxy: see /var/log/mail.log (also see "pimpstat")}
\item{IMAP server: see /var/log/auth (and /var/log/syslog)}
\end{itemize}
=> if contains "<time> mail imapd[22677]: Command stream end of file, while reading line user=\ldots host=\ldots\newline
                  <time> mail PAM\_cas[22732]: authentication failure for user '\ldots' : bad CAS ticket."\newline
     then check /etc/pam.d/imapd whether the "old" PAM login module (system-auth? shadow?) is set to "sufficient" (NOT to "required"!)

\begin{itemize}
\item{CAS server: see \$TOMCAT/logs/cas3-server.log}
\end{itemize}
\end{document}
