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