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):
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.
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
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
//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.
@@ -132,6 +132,19 @@
+
+
+ cas.kuleuven.be
+ 443
+ cas
+ https://webmail.kuleuven.be/horde3/casProxy.php
+ /tmp
+ false
+ false
+ /tmp/phpCAS.log
+
+
+
steps
*) redirect on logout (highly recommended)
...
$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)
//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:
$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!