Project

General

Profile

cas_authentication.php

template cas_authentication.php - Emmanuel GARETTE (2), 08/31/2011 07:32 PM

Download (10.2 KB)

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