6.0.0-git
2024-03-19
Last Modified 2013-10-24 by Thomas Jarosch

CAS Authentication HowTo

Jan Van der Velpen aka Velpi (who did all the work)
Peter Arien aka Kaos99 (who just likes playing around with Horde)
Thanks go to the Ja-Sig and the ESUP people!!

Our university is working towards a complete AAI (Authentication and Authorization Infrastructure) implementation. For web applications we are using the Shibboleth architecture. But as you can read in the Shibboleth Authentication HowTo, 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) 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/

First we used the ESUP pam module (referenced here) 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:
/etc/pam.conf:

 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

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

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

/etc/pam_cas.conf:

 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

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.
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, ...]
############################

Tested succesfully using standard Debian packages [20051206]

  • Horde 3.0.4-4
  • IMP 4.0.2-2

Connection problems when using horde from CVS [framework_3 20051220]
Tested succesfully using standard horde release packages [20060620]

  • Horde 3.1.1
  • IMP 4.1.2
  • phpCAS 0.4.22-RC with patches (see below)

Tested succesfully using standard horde release packages [20081009]

  • Horde 3.1.3
  • IMP 4.1.3
  • phpCAS 0.6.0

First, install a basic horde system
Configure it to use IMAP auth for horde-auth
Set imp/conf/servers.php correctly for your backend and set 'hordeauth' => true
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.
We use up-imapproxy from http://www.imapproxy.org/ .

Check your current system so everything works at this point (DO IT!)
Now we can start patching it to use CAS
(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...)

1) configure Apache
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.
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.
-----------httpd.conf------------
SSLCertificateFile /etc/pki/myHORDEserver.pem
SSLCertificateChainFile /etc/pki/ca_cert.pem
#added for the trust mechanism----
SSLCACertificateFile /etc/pki/ca_cert.pem
#----added


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/]

2) install phpCAS library in horde
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.
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:
http://www.ja-sig.org/wiki/display/CASC/phpCAS
(extract the package and)
[change the path to your horde/php lib dir accordingly]
phpCAS 0.6 and lower:

mkdir $HORDE_DIR/lib/CAS/ 
cp -r $PHPCAS_SOURCE_DIR/CAS/* $HORDE_DIR/lib/CAS/

phpCAS 1.0 and higher (DOES NOT WORK: K.U.Leuven driver needs adjustments!! To be continued...; 20081009):
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/

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

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

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

  • hordeauth=true => horde "pass" will be a PT that phpCAS has already requested
  • 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]

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();

+*/

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



Please note that CAS will request a PT for the service that it is trying to connect to.
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!
(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
notes:

  • horde uid (login name) will be the CAS netId when authenticated
  • CAS does no authorisation, everybody that can login to CAS, can enter horde (if no extra measures are taken, see next topic)
enable configuration settings for horde auth:
---------horde/config/conf.xml---------
@@ -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


8) patch horde's hooks if you want authorisation (=check user with another backend)
note: this has nothing to do with AUTHENTICATION! Meaning you don't need this to get CAS working.
note: this is a configurable option (horde config.php: $conf['auth']['params']['authorisation'])
don't forget to configure this correctly if you want to use it (eg LDAP settings)
---------horde/config/hooks.php---------
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);
     }
}


[optional steps]

*) redirect on logout (highly recommended)
Logging out is a little less easy when using a WebISO since it will automatically re-login when there is still a session with the central server.
A simple workaround is to make the redirect on logout link to a location that doesn't need authentication.
----horde/config/conf.php----
...
$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';
...


*) adjust the standard login page (recommended)
You might want to adjust this page so it doesn't show a login box when using CAS.
----horde/config/conf.php----
//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';


--INSTALL COMPLETED--

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

 $this->_imapService = $p."://".$servers[$server]['server'];

the code

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

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

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

DEBUG HINTS:

  • horde uid (login name) will be the CAS netId when authenticated
  • 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)

=> 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

  • IMAP proxy: see /var/log/mail.log (also see "pimpstat")
  • IMAP server: see /var/log/auth (and /var/log/syslog)

=> if contains "<time> mail imapd[22677]: Command stream end of file, while reading line user=... host=...
<time> mail PAM_cas[22732]: authentication failure for user '...' : bad CAS ticket."
then check /etc/pam.d/imapd whether the "old" PAM login module (system-auth? shadow?) is set to "sufficient" (NOT to "required"!)

  • CAS server: see $TOMCAT/logs/cas3-server.log