cas_authentication.php
| 1 |
<?php
|
|---|---|
| 2 |
/**
|
| 3 |
* CAS Authentication
|
| 4 |
*
|
| 5 |
* This plugin replaces the RoundCube login page with authentication requests
|
| 6 |
* to a CAS server, which enables logging into RoundCube with identities
|
| 7 |
* authenticated by the CAS server and acts as a CAS proxy to relay authenticated
|
| 8 |
* credentials to the IMAP backend.
|
| 9 |
*
|
| 10 |
* @version 0.4.2
|
| 11 |
* @author Alex Li (li@hcs.harvard.edu)
|
| 12 |
*
|
| 13 |
*/
|
| 14 |
|
| 15 |
class cas_authentication extends rcube_plugin { |
| 16 |
// fields
|
| 17 |
private $cas_inited; |
| 18 |
|
| 19 |
/**
|
| 20 |
* Initialize plugin
|
| 21 |
*
|
| 22 |
*/
|
| 23 |
function init() { |
| 24 |
// initialize plugin fields
|
| 25 |
$cas_inited = false; |
| 26 |
|
| 27 |
// load plugin configurations
|
| 28 |
$this->load_config();
|
| 29 |
|
| 30 |
// add application hooks
|
| 31 |
$this->add_hook('startup', array($this, 'startup')); |
| 32 |
$this->add_hook('render_page', array($this, 'render_page')); |
| 33 |
$this->add_hook('authenticate', array($this, 'authenticate')); |
| 34 |
$this->add_hook('login_after', array($this, 'login_after')); |
| 35 |
$this->add_hook('login_failed', array($this, 'login_failed')); |
| 36 |
$this->add_hook('logout_after', array($this, 'logout_after')); |
| 37 |
$this->add_hook('imap_connect', array($this, 'imap_connect')); |
| 38 |
} |
| 39 |
|
| 40 |
/**
|
| 41 |
* Handle plugin-specific actions
|
| 42 |
* These actions are handled at the startup hook rather than registered as
|
| 43 |
* custom actions because the user session does not necessarily exist when
|
| 44 |
* these actions need to be handled.
|
| 45 |
*
|
| 46 |
* @param array $args arguments from rcmail
|
| 47 |
* @return array modified arguments
|
| 48 |
*/
|
| 49 |
function startup($args) { |
| 50 |
// intercept PGT callback action
|
| 51 |
if ($args['action'] == 'pgtcallback') { |
| 52 |
// initialize CAS client
|
| 53 |
$this->cas_init();
|
| 54 |
|
| 55 |
// retrieve and store PGT if present
|
| 56 |
eolephpCAS::forceAuthentication(); |
| 57 |
|
| 58 |
// end script
|
| 59 |
exit;
|
| 60 |
} |
| 61 |
|
| 62 |
// intercept CAS logout action
|
| 63 |
else if ($args['action'] == 'caslogout') { |
| 64 |
// initialize CAS client
|
| 65 |
$this->cas_init();
|
| 66 |
|
| 67 |
// logout from CAS server
|
| 68 |
eolephpCAS::logout(); |
| 69 |
|
| 70 |
// end script
|
| 71 |
exit;
|
| 72 |
} |
| 73 |
|
| 74 |
return $args; |
| 75 |
} |
| 76 |
|
| 77 |
/**
|
| 78 |
* Intercept page rendering
|
| 79 |
*
|
| 80 |
* @param array $args arguments from rcmail
|
| 81 |
* @return array modified arguments
|
| 82 |
*/
|
| 83 |
function render_page($args) { |
| 84 |
// intercept login template rendering in order to replace login form with CAS request
|
| 85 |
if ($args['template'] == 'login') { |
| 86 |
// retrieve rcmail instance
|
| 87 |
$rcmail = rcmail::get_instance();
|
| 88 |
|
| 89 |
// save request url to a cookie
|
| 90 |
$url = get_input_value('_url', RCUBE_INPUT_POST); |
| 91 |
if (empty($url) && !preg_match('/_task=logout/', $_SERVER['QUERY_STRING'])) { |
| 92 |
$url = $_SERVER['QUERY_STRING']; |
| 93 |
} |
| 94 |
setcookie('cas_url', $url); |
| 95 |
|
| 96 |
// redirect to login action
|
| 97 |
$rcmail->output->redirect(array('action' => 'login', 'task' => 'mail')); |
| 98 |
} |
| 99 |
|
| 100 |
return $args; |
| 101 |
} |
| 102 |
|
| 103 |
/**
|
| 104 |
* Inject authentication credentials
|
| 105 |
*
|
| 106 |
* @param array $args arguments from rcmail
|
| 107 |
* @return array modified arguments
|
| 108 |
*/
|
| 109 |
function authenticate($args) { |
| 110 |
// retrieve configurations
|
| 111 |
$cfg = rcmail::get_instance()->config->all();
|
| 112 |
|
| 113 |
// initialize CAS client
|
| 114 |
$this->cas_init();
|
| 115 |
|
| 116 |
// attempt to authenticate with CAS server
|
| 117 |
if (eolephpCAS::forceAuthentication()) {
|
| 118 |
// retrieve authenticated credentials
|
| 119 |
$args['user'] = eolephpCAS::getUser(); |
| 120 |
if ($cfg['cas_proxy']) { |
| 121 |
$args['pass'] = ''; |
| 122 |
} |
| 123 |
else {
|
| 124 |
$args['pass'] = $cfg['cas_imap_password']; |
| 125 |
} |
| 126 |
} |
| 127 |
|
| 128 |
return $args; |
| 129 |
} |
| 130 |
|
| 131 |
/**
|
| 132 |
* Inject post-login redirection url
|
| 133 |
*
|
| 134 |
* @param array $args arguments from rcmail
|
| 135 |
* @return array modified arguments
|
| 136 |
*/
|
| 137 |
function login_after($args) { |
| 138 |
// restore original request parameters
|
| 139 |
$query = array(); |
| 140 |
if ($url = $_COOKIE['cas_url']) { |
| 141 |
parse_str($url, $query); |
| 142 |
$args = $query; |
| 143 |
} |
| 144 |
|
| 145 |
return $args; |
| 146 |
} |
| 147 |
|
| 148 |
/**
|
| 149 |
* Intercept login failure
|
| 150 |
*
|
| 151 |
* @param array $args arguments from rcmail
|
| 152 |
* @return array modified arguments
|
| 153 |
*/
|
| 154 |
function login_failed($args) { |
| 155 |
// retrieve rcmail instance
|
| 156 |
$rcmail = rcmail::get_instance();
|
| 157 |
|
| 158 |
// compose error page content
|
| 159 |
global $__page_content, $__error_title, $__error_text; |
| 160 |
$__error_title = "IMAP LOGIN FAILED"; |
| 161 |
$__error_text = <<<EOF |
| 162 |
Could not log into your IMAP service. The service may be interrupted, or you may not be authorized to access the service.<br />
|
| 163 |
Please contact the administrator of your IMAP service.<br />
|
| 164 |
Or log out by clicking on the button below, then try again with a different user name.<br />
|
| 165 |
EOF;
|
| 166 |
$__page_content = <<<EOF |
| 167 |
<div>
|
| 168 |
<h3 class="error-title">$__error_title</h3>
|
| 169 |
<p class="error-text">$__error_text</p>
|
| 170 |
<form name="form" action="./" method="get">
|
| 171 |
<input type="hidden" name="_action" value="caslogout" />
|
| 172 |
<p style="text-align:center;"><input type="submit" class="button mainaction" value="Logout" /></p>
|
| 173 |
</form>
|
| 174 |
</div>
|
| 175 |
EOF;
|
| 176 |
|
| 177 |
// redirect to error page
|
| 178 |
$rcmail->output->reset(); |
| 179 |
$rcmail->output->send('error'); |
| 180 |
|
| 181 |
// kill current session
|
| 182 |
$rcmail->kill_session();
|
| 183 |
|
| 184 |
// end script
|
| 185 |
exit;
|
| 186 |
} |
| 187 |
|
| 188 |
/**
|
| 189 |
* Perform post-logout actions
|
| 190 |
*
|
| 191 |
* @param array $args arguments from rcmail
|
| 192 |
* @return array modified arguments
|
| 193 |
*/
|
| 194 |
function logout_after($args) { |
| 195 |
// retrieve rcmail instance
|
| 196 |
$rcmail = rcmail::get_instance();
|
| 197 |
|
| 198 |
// redirect to CAS logout action
|
| 199 |
$rcmail->output->redirect(array('action' => 'caslogout')); |
| 200 |
} |
| 201 |
|
| 202 |
/**
|
| 203 |
* Inject IMAP authentication credentials
|
| 204 |
*
|
| 205 |
* @param array $args arguments from rcmail
|
| 206 |
* @return array modified arguments
|
| 207 |
*/
|
| 208 |
function imap_connect($args) { |
| 209 |
// retrieve configurations
|
| 210 |
$cfg = rcmail::get_instance()->config->all();
|
| 211 |
|
| 212 |
// RoundCube is acting as CAS proxy
|
| 213 |
if ($cfg['cas_proxy']) { |
| 214 |
// a proxy ticket has been retrieved, the IMAP server caches proxy tickets, and this is the first connection attempt
|
| 215 |
if ($_SESSION['cas_pt'][php_uname('n')] && $cfg['cas_imap_caching'] && $args['attempt'] == 1) { |
| 216 |
// use existing proxy ticket in session
|
| 217 |
$args['pass'] = $_SESSION['cas_pt'][php_uname('n')]; |
| 218 |
} |
| 219 |
|
| 220 |
// no proxy tickets have been retrieved, the IMAP server doesn't cache proxy tickets, or the first connection attempt has failed
|
| 221 |
else {
|
| 222 |
// initialize CAS client
|
| 223 |
$this->cas_init();
|
| 224 |
|
| 225 |
// retrieve a new proxy ticket and store it in session
|
| 226 |
$_SESSION['cas_pt'][php_uname('n')] = eolephpCAS::retrievePT($cfg['cas_imap_name'], $err_code, $output); |
| 227 |
$args['pass'] = $_SESSION['cas_pt'][php_uname('n')]; |
| 228 |
} |
| 229 |
|
| 230 |
// enable retry on the first connection attempt only
|
| 231 |
if ($args['attempt'] <= 1) { |
| 232 |
$args['retry'] = true; |
| 233 |
} |
| 234 |
} |
| 235 |
|
| 236 |
return $args; |
| 237 |
} |
| 238 |
|
| 239 |
/**
|
| 240 |
* Initialize CAS client
|
| 241 |
*
|
| 242 |
*/
|
| 243 |
private function cas_init() { |
| 244 |
if (!$this->cas_inited) { |
| 245 |
// retrieve configurations
|
| 246 |
$cfg = rcmail::get_instance()->config->all();
|
| 247 |
|
| 248 |
// include eolephpCAS
|
| 249 |
require_once('CAS/eoleCAS.php'); |
| 250 |
|
| 251 |
// initialize CAS client
|
| 252 |
if ($cfg['cas_proxy']) { |
| 253 |
eolephpCAS::proxy(CAS_VERSION_2_0, $cfg['cas_hostname'], $cfg['cas_port'], $cfg['cas_uri'], false); |
| 254 |
//eolephpCAS::setDebug('/tmp/pouet.txt');
|
| 255 |
|
| 256 |
// set URL for PGT callback
|
| 257 |
eolephpCAS::setFixedCallbackURL($this->generate_url(array('action' => 'pgtcallback'))); |
| 258 |
|
| 259 |
// set PGT storage
|
| 260 |
eolephpCAS::setPGTStorageFile('xml', $cfg['cas_pgt_dir']); |
| 261 |
} |
| 262 |
else {
|
| 263 |
eolephpCAS::client(CAS_VERSION_2_0, $cfg['cas_hostname'], $cfg['cas_port'], $cfg['cas_uri'], false); |
| 264 |
} |
| 265 |
|
| 266 |
// set service URL for authorization with CAS server
|
| 267 |
eolephpCAS::setFixedServiceURL($this->generate_url(array('action' => 'login', 'task' => 'mail'))); |
| 268 |
|
| 269 |
// set SSL validation for the CAS server
|
| 270 |
if ($cfg['cas_validation'] == 'self') { |
| 271 |
eolephpCAS::setCasServerCert($cfg['cas_cert']); |
| 272 |
} |
| 273 |
else if ($cfg['cas_validation'] == 'ca') { |
| 274 |
eolephpCAS::setCasServerCACert($cfg['cas_cert']); |
| 275 |
} |
| 276 |
else {
|
| 277 |
eolephpCAS::setNoCasServerValidation(); |
| 278 |
} |
| 279 |
|
| 280 |
// set login and logout URLs of the CAS server
|
| 281 |
eolephpCAS::setServerLoginURL($cfg['cas_login_url']); |
| 282 |
eolephpCAS::setServerLogoutURL($cfg['cas_logout_url']); |
| 283 |
|
| 284 |
$this->cas_inited = true; |
| 285 |
} |
| 286 |
} |
| 287 |
|
| 288 |
/**
|
| 289 |
* Build full URLs to this instance of RoundCube for use with CAS servers
|
| 290 |
*
|
| 291 |
* @param array $params url parameters as key-value pairs
|
| 292 |
* @return string full Roundcube URL
|
| 293 |
*/
|
| 294 |
private function generate_url($params) { |
| 295 |
$s = ($_SERVER['HTTPS'] == 'on') ? 's' : ''; |
| 296 |
$protocol = $this->strleft(strtolower($_SERVER['SERVER_PROTOCOL']), '/') . $s; |
| 297 |
$port = (($_SERVER['SERVER_PORT'] == '80' && $_SERVER['HTTPS'] != 'on') || |
| 298 |
($_SERVER['SERVER_PORT'] == '443' && $_SERVER['HTTPS'] == 'on')) ? |
| 299 |
'' : (':' .$_SERVER['SERVER_PORT']); |
| 300 |
$path = $this->strleft($_SERVER['REQUEST_URI'], '?'); |
| 301 |
$parsed_params = ''; |
| 302 |
$delm = '?'; |
| 303 |
foreach (array_reverse($params) as $key => $val) { |
| 304 |
if (!empty($val)) { |
| 305 |
$parsed_key = $key[0] == '_' ? $key : '_' . $key; |
| 306 |
$parsed_params .= $delm . urlencode($parsed_key) . '=' . urlencode($val); |
| 307 |
$delm = '&'; |
| 308 |
} |
| 309 |
} |
| 310 |
return $protocol . '://' . $_SERVER['SERVER_NAME'] . $port . $path . $parsed_params; |
| 311 |
} |
| 312 |
|
| 313 |
private function strleft($s1, $s2) { |
| 314 |
$length = strpos($s1, $s2); |
| 315 |
if ($length) { |
| 316 |
return substr($s1, 0, $length); |
| 317 |
} |
| 318 |
else {
|
| 319 |
return $s1; |
| 320 |
} |
| 321 |
} |
| 322 |
} |
| 323 |
?>
|