lib.php 24.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
/**
 * Mahara: Electronic portfolio, weblog, resume builder and social networking
 * Copyright (C) 2006-2009 Catalyst IT Ltd (http://www.catalyst.net.nz)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package    mahara
 * @subpackage auth-saml
 * @author     Piers Harding <piers@catalyst.net.nz>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
 * @copyright  (C) 2009 Catalyst IT Ltd http://catalyst.net.nz
 * @copyright  (C) portions from Moodle, (C) Martin Dougiamas http://dougiamas.com
 */

defined('INTERNAL') || die();
require_once(get_config('docroot') . 'auth/lib.php');

/**
 * Authenticates users with SAML 2.0
 */
class AuthSaml extends Auth {

    public function __construct($id = null) {
        $this->type = 'saml';
        $this->has_instance_config = true;
38

39
40
41
42
43
44
45
46
47
48
49
        $this->config['simplesamlphplib'] = get_config_plugin('auth', 'saml', 'simplesamlphplib');
        $this->config['simplesamlphpconfig'] = get_config_plugin('auth', 'saml', 'simplesamlphpconfig');
        $this->config['user_attribute'] = '';
        $this->config['weautocreateusers'] = 1;
        $this->config['firstnamefield' ] = '';
        $this->config['surnamefield'] = '';
        $this->config['emailfield'] = '';
        $this->config['institutionattribute'] = '';
        $this->config['institutionregex'] = 0;
        $this->config['institutionvalue'] = '';
        $this->config['updateuserinfoonlogin'] = 1;
50
        $this->config['remoteuser'] = true;
51
        $this->config['loginlink'] = false;
52
        $this->instanceid = $id;
53

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
        if (!empty($id)) {
            return $this->init($id);
        }
        return true;
    }

    public function init($id = null) {
        $this->ready = parent::init($id);

        // Check that required fields are set
        if ( empty($this->config['user_attribute']) ||
             empty($this->config['institutionattribute'])
              ) {
            $this->ready = false;
        }

        return $this->ready;
    }


    /**
     * We can autocreate users if the admin has said we can
     * in weautocreateusers
     */
    public function can_auto_create_users() {
79
        return (bool)$this->config['weautocreateusers'];
80
81
82
83
84
85
86
87
88
    }


    /**
     * Grab a delegate object for auth stuff
     */
    public function request_user_authorise($attributes) {
        global $USER, $SESSION;
        $this->must_be_ready();
89

90
91
92
93
        if (empty($attributes) or !array_key_exists($this->config['user_attribute'], $attributes)
                               or !array_key_exists($this->config['institutionattribute'], $attributes)) {
            throw new AccessDeniedException();
        }
94

95
        $remoteuser      = $attributes[$this->config['user_attribute']][0];
96
97
98
        $firstname       = isset($attributes[$this->config['firstnamefield']][0]) ? $attributes[$this->config['firstnamefield']][0] : null;
        $lastname        = isset($attributes[$this->config['surnamefield']][0]) ? $attributes[$this->config['surnamefield']][0] : null;
        $email           = isset($attributes[$this->config['emailfield']][0]) ? $attributes[$this->config['emailfield']][0] : null;
99
        $institutionname = $this->institution;
100

101
102
103
104
105
        $create = false;
        $update = false;

        // Retrieve a $user object. If that fails, create a blank one.
        try {
106
            $isremote = $this->config['remoteuser'] ? true : false;
107
108
            $user = new User;
            if (get_config('usersuniquebyusername')) {
109
110
                // When turned on, this setting means that it doesn't matter
                // which other application the user SSOs from, they will be
111
112
                // given the same account in Mahara.
                //
113
114
115
116
117
                // This setting is one that has security implications unless
                // only turned on by people who know what they're doing. In
                // particular, every system linked to Mahara should be making
                // sure that same username == same person.  This happens for
                // example if two Moodles are using the same LDAP server for
118
119
                // authentication.
                //
120
121
122
                // If this setting is on, it must NOT be possible to self
                // register on the site for ANY institution - otherwise users
                // could simply pick usernames of people's accounts they wished
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
                // to steal.
                if ($institutions = get_column('institution', 'name', 'registerallowed', '1')) {
                    log_warn("usersuniquebyusername is turned on but registration is allowed for an institution. "
                        . "No institution can have registration allowed for it, for security reasons.\n"
                        . "The following institutions have registration enabled:\n  " . join("\n  ", $institutions));
                    throw new AccessDeniedException();
                }

                if (!get_config('usersallowedmultipleinstitutions')) {
                    log_warn("usersuniquebyusername is turned on but usersallowedmultipleinstitutions is off. "
                        . "This makes no sense, as users will then change institution every time they log in from "
                        . "somewhere else. Please turn this setting on in Site Options");
                    throw new AccessDeniedException();
                }
            }
138
139
140
141
142
143
144
145
            else {
                if (!$isremote){
                    log_warn("usersuniquebyusername is turned off but remoteuser has not been set on for this institution: $institutionname. "
                        . "This is a security risk as users from different institutions with different IdPs can hijack "
                        . "each others accounts.  Fix this in the institution level auth/saml settings.");
                    throw new AccessDeniedException();
                }
            }
146
147
148
149
150
151
            if ($isremote) {
                $user->find_by_instanceid_username($this->instanceid, $remoteuser, $isremote);
            }
            else {
                $user->find_by_username($remoteuser);
            }
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186

            if ($user->get('suspendedcusr')) {
                die_info(get_string('accountsuspended', 'mahara', strftime(get_string('strftimedaydate'), $user->get('suspendedctime')), $user->get('suspendedreason')));
            }

            if ('1' == $this->config['updateuserinfoonlogin']) {
                $update = true;
            }
        } catch (AuthUnknownUserException $e) {
            if (!empty($this->config['weautocreateusers'])) {
                $institution = new Institution($this->institution);
                if ($institution->isFull()) {
                    throw new XmlrpcClientException('SSO attempt from ' . $institution->displayname . ' failed - institution is full');
                }
                $user = new User;
                $create = true;
            }
            else {
                log_debug("User authorisation request from SAML failed - "
                    . "remote user '$remoteuser' is unknown to us and auto creation of users is turned off");
                return false;
            }
        }

        /*******************************************/

        if ($create) {

            $user->passwordchange     = 1;
            $user->active             = 1;
            $user->deleted            = 0;

            $user->expiry             = null;
            $user->expirymailsent     = 0;
            $user->lastlogin          = time();
187

188
189
190
191
            $user->firstname          = $firstname;
            $user->lastname           = $lastname;
            $user->email              = $email;

192
193
            // must have these values
            if (empty($firstname) || empty($lastname) || empty($email)) {
194
                throw new AccessDeniedException(get_string('errormissinguserattributes', 'auth.saml'));
195
196
            }

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
            $user->authinstance       = empty($this->config['parent']) ? $this->instanceid : $this->parent;

            db_begin();
            $user->username           = get_new_username($remoteuser);

            $user->id = create_user($user, array(), $institutionname, $this, $remoteuser);

            /*
             * We need to convert the object to a stdclass with its own
             * custom method because it uses overloaders in its implementation
             * and its properties wouldn't be visible to a simple cast operation
             * like (array)$user
             */
            $userobj = $user->to_stdclass();
            $userarray = (array)$userobj;
            db_commit();

214
            // Now we have fired the create event, we need to re-get the data
215
216
217
218
            // for this user
            $user = new User;
            $user->find_by_id($userobj->id);

219
220
221
222
223
            if (get_config('usersuniquebyusername')) {
                // Add them to the institution they have SSOed in by
                $user->join_institution($institutionname);
            }

224
        } elseif ($update) {
225
226
227
228
229
230
231
232
233
234
235
            if (! empty($firstname)) {
                set_profile_field($user->id, 'firstname', $firstname);
                $user->firstname = $firstname;
            }
            if (! empty($lastname)) {
                set_profile_field($user->id, 'lastname', $lastname);
                $user->lastname = $lastname;
            }
            if (! empty($email)) {
                set_profile_field($user->id, 'email', $email);
                $user->email = $email;
236
            }
237
238
239
            $user->lastlastlogin      = $user->lastlogin;
            $user->lastlogin          = time();
        }
240
        $user->commit();
241
242
243
244
245
246


        /*******************************************/

        // We know who our user is now. Bring em back to life.
        $result = $USER->reanimate($user->id, $this->instanceid);
247
        log_debug("remote user '$remoteuser' is now reanimated as '{$USER->username}' ");
248
249
250
251
        $SESSION->set('authinstance', $this->instanceid);

        return true;
    }
252

253
254
255
    // ensure that a user is logged out of mahara and the SAML 2.0 IdP
    public function logout() {
        global $CFG, $USER, $SESSION;
256

257
258
        // logout of mahara
        $USER->logout();
259

260
261
        // tidy up the session for retries
        $SESSION->set('messages', array());
262
        $SESSION->set('wantsurl', null);
263
264
265
266
        
        // redirect for logout of SAML 2.0 IdP
        redirect($CFG->wwwroot.'/auth/saml/?logout=1');
    }
267

268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
    public function after_auth_setup_page_hook() {
        return;
    }
}

/**
 * Plugin configuration class
 */
class PluginAuthSaml extends PluginAuth {

    private static $default_config = array(
//        'idpidentity'           => '',
        'simplesamlphplib'      => '',
        'simplesamlphpconfig'   => '',
        'user_attribute'        => '',
283
        'weautocreateusers'     => 0,
284
285
286
287
288
289
290
291
        'firstnamefield'        => '',
        'surnamefield'          => '',
        'emailfield'            => '',
        'updateuserinfoonlogin' => 1,
        'institution'           => '',
        'institutionattribute'  => '',
        'institutionvalue'      => '',
        'institutionregex'      => 0,
292
        'remoteuser'            => 1,
293
        'loginlink'             => 0,
294
295
            );

296
297
298
299
    public static function can_be_disabled() {
        return true;
    }

300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
    public static function has_config() {
        return true;
    }

    public static function get_config_options() {

        $elements = array(
            'authname' => array(
                'type'  => 'hidden',
                'value' => 'saml',
            ),
            'authglobalconfig' => array(
                'type'  => 'hidden',
                'value' => 'saml',
            ),
            'simplesamlphplib' => array(
                'type'  => 'text',
                'size' => 50,
                'title' => get_string('simplesamlphplib', 'auth.saml'),
                'rules' => array(
                    'required' => true,
                ),
                'defaultvalue' => get_config_plugin('auth', 'saml', 'simplesamlphplib'),
                'help'  => true,
            ),
            'simplesamlphpconfig' => array(
                'type'  => 'text',
                'size' => 50,
                'title' => get_string('simplesamlphpconfig', 'auth.saml'),
                'rules' => array(
                    'required' => true,
                ),
                'defaultvalue' => get_config_plugin('auth', 'saml', 'simplesamlphpconfig'),
                'help'  => true,
            ),
        );

        return array(
            'elements' => $elements,
            'renderer' => 'table'
        );
    }

    public static function has_instance_config() {
        return true;
    }

    public static function is_usable() {
        // would be good to be able to detect SimpleSAMLPHP libraries
        return true;
    }

    public static function get_instance_config_options($institution, $instance = 0) {

        if ($instance > 0) {
            $default = get_record('auth_instance', 'id', $instance);
            if ($default == false) {
                throw new SystemException('Could not find data for auth instance ' . $instance);
            }
            $current_config = get_records_menu('auth_instance_config', 'instance', $instance, '', 'field, value');

            if ($current_config == false) {
                $current_config = array();
            }

            foreach (self::$default_config as $key => $value) {
                if (array_key_exists($key, $current_config)) {
                    self::$default_config[$key] = $current_config[$key];
                }
            }
            if(empty(self::$default_config['institutionvalue'])) {
                self::$default_config['institutionvalue'] = $institution;
            }
        } else {
            $default = new stdClass();
            $default->instancename = '';
        }

        $elements = array(
            'instance' => array(
                'type'  => 'hidden',
                'value' => $instance,
            ),
383
384
385
386
            'instancename' => array(
                'type'  => 'hidden',
                'value' => 'SAML',
            ),
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
            'institution' => array(
                'type'  => 'hidden',
                'value' => $institution,
            ),
            'authname' => array(
                'type'  => 'hidden',
                'value' => 'saml',
            ),
            'institutionattribute' => array(
                'type'  => 'text',
                'title' => get_string('institutionattribute', 'auth.saml', $institution),
                'rules' => array(
                    'required' => true,
                ),
                'defaultvalue' => self::$default_config['institutionattribute'],
                'help' => true,
            ),
            'institutionvalue' => array(
                'type'  => 'text',
                'title' => get_string('institutionvalue', 'auth.saml'),
                'rules' => array(
                'required' => true,
                ),
                'defaultvalue' => self::$default_config['institutionvalue'],
                'help' => true,
            ),
            'institutionregex' => array(
                'type'  => 'checkbox',
                'title' => get_string('institutionregex', 'auth.saml'),
                'defaultvalue' => self::$default_config['institutionregex'],
                'help' => true,
            ),
            'user_attribute' => array(
                'type'  => 'text',
                'title' => get_string('userattribute', 'auth.saml'),
                'rules' => array(
                    'required' => true,
                ),
                'defaultvalue' => self::$default_config['user_attribute'],
                'help' => true,
            ),
428
429
430
431
432
433
            'remoteuser' => array(
                'type'  => 'checkbox',
                'title' => get_string('remoteuser', 'auth.saml'),
                'defaultvalue' => self::$default_config['remoteuser'],
                'help'  => true,
            ),
434
435
436
437
438
439
440
            'loginlink' => array(
                'type'  => 'checkbox',
                'title' => get_string('loginlink', 'auth.saml'),
                'defaultvalue' => self::$default_config['loginlink'],
                'disabled' => (self::$default_config['remoteuser'] ? false : true),
                'help'  => true,
            ),
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
            'updateuserinfoonlogin' => array(
                'type'  => 'checkbox',
                'title' => get_string('updateuserinfoonlogin', 'auth.saml'),
                'defaultvalue' => self::$default_config['updateuserinfoonlogin'],
                'help'  => true,
            ),
            'weautocreateusers' => array(
                'type'  => 'checkbox',
                'title' => get_string('weautocreateusers', 'auth.saml'),
                'defaultvalue' => self::$default_config['weautocreateusers'],
                'help'  => true,
            ),
            'firstnamefield' => array(
                'type'  => 'text',
                'title' => get_string('samlfieldforfirstname', 'auth.saml'),
                'defaultvalue' => self::$default_config['firstnamefield'],
                'help'  => true,
            ),
            'surnamefield' => array(
                'type'  => 'text',
                'title' => get_string('samlfieldforsurname', 'auth.saml'),
                'defaultvalue' => self::$default_config['surnamefield'],
                'help'  => true,
            ),
            'emailfield' => array(
                'type'  => 'text',
                'title' => get_string('samlfieldforemail', 'auth.saml'),
                'defaultvalue' => self::$default_config['emailfield'],
                'help' => true,
            ),
        );

        return array(
            'elements' => $elements,
            'renderer' => 'table'
        );
    }


    public static function validate_config_options($values, $form) {

482
483
484
485
486
        if ($values instanceof Pieform) {
            $tmp = $form;
            $form = $values;
            $values = $tmp;
        }
487
488
489
490
491
492
493
494
495
496
497
        // fix problems with config validation interface incorrect between site/institution
        if (isset($values['authglobalconfig'])) {
            // SimpleSAMLPHP lib directory must have right things
            if (! file_exists($values['simplesamlphplib'].'/lib/_autoload.php')) {
                $form->set_error('simplesamlphplib', get_string('errorbadlib', 'auth.saml', $values['simplesamlphplib']));
            }
            // SimpleSAMLPHP config directory must shape up
            if (! file_exists($values['simplesamlphpconfig'].'/config.php')) {
                $form->set_error('simplesamlphpconfig', get_string('errorbadconfig', 'auth.saml', $values['simplesamlphpconfig']));
            }
        }
498
499
500
501
        // only allow remoteuser to be unset if usersuniquebyusername is NOT set
        if (isset($values['remoteuser']) && !get_config('usersuniquebyusername') && !$values['remoteuser']) {
            $form->set_error('remoteuser', get_string('errorremoteuser', 'auth.saml'));
        }
502
503
504
505
506
        if (isset($values['weautocreateusers'])) {
            if ($values['weautocreateusers'] && $values['remoteuser']) {
                $form->set_error('weautocreateusers', get_string('errorbadcombo', 'auth.saml'));
            }
        }
507
508
509
510
511
512
513
514
515
516
        $dup = get_records_sql_array('SELECT COUNT(instance) AS instance FROM {auth_instance_config}
                                          WHERE ((field = \'institutionattribute\' AND value = ?) OR
                                                 (field = \'institutionvalue\' AND value = ?)) AND
                                                 instance IN (SELECT id FROM {auth_instance} WHERE authname = \'saml\' AND id != ?)
                                          GROUP BY instance
                                          ORDER BY instance',
                                      array($values['institutionattribute'], $values['institutionvalue'], $values['instance']));
        foreach ($dup as $instance) {
            if ($instance->instance >= 2) {
                // we already have an authinstance with these same values
517
                $form->set_error('institutionattribute', get_string('errorbadinstitutioncombo', 'auth.saml'));
518
519
520
                break;
            }
        }
521
    }
522
523


524
525
526
    public static function save_config_options($values, $form) {

        $configs = array('simplesamlphplib', 'simplesamlphpconfig');
527

528
529
530
531
532
533
534
        if (isset($values['authglobalconfig'])) {
            foreach ($configs as $config) {
                set_config_plugin('auth', 'saml', $config, $values[$config]);
            }
        }
        else {
            $authinstance = new stdClass();
535

536
537
538
539
540
541
542
543
            if ($values['instance'] > 0) {
                $values['create'] = false;
                $current = get_records_assoc('auth_instance_config', 'instance', $values['instance'], '', 'field, value');
                $authinstance->id = $values['instance'];
            }
            else {
                $values['create'] = true;
                $lastinstance = get_records_array('auth_instance', 'institution', $values['institution'], 'priority DESC', '*', '0', '1');
544

545
546
547
548
549
550
551
                if ($lastinstance == false) {
                    $authinstance->priority = 0;
                }
                else {
                    $authinstance->priority = $lastinstance[0]->priority + 1;
                }
            }
552

553
554
555
            $authinstance->institution  = $values['institution'];
            $authinstance->authname     = $values['authname'];
            $authinstance->instancename = $values['authname'];
556

557
558
559
560
561
562
            if ($values['create']) {
                $values['instance'] = insert_record('auth_instance', $authinstance, 'id', true);
            }
            else {
                update_record('auth_instance', $authinstance, array('id' => $values['instance']));
            }
563

564
565
566
            if (empty($current)) {
                $current = array();
            }
567

568
569
            self::$default_config =   array('user_attribute' => $values['user_attribute'],
                                            'weautocreateusers' => $values['weautocreateusers'],
570
                                            'loginlink' => $values['loginlink'],
571
                                            'remoteuser' => $values['remoteuser'],
572
573
574
575
576
577
578
579
                                            'firstnamefield' => $values['firstnamefield'],
                                            'surnamefield' => $values['surnamefield'],
                                            'emailfield' => $values['emailfield'],
                                            'updateuserinfoonlogin' => $values['updateuserinfoonlogin'],
                                            'institutionattribute' => $values['institutionattribute'],
                                            'institutionvalue' => $values['institutionvalue'],
                                            'institutionregex' => $values['institutionregex'],
                                            );
580

581
582
583
584
585
            foreach(self::$default_config as $field => $value) {
                $record = new stdClass();
                $record->instance = $values['instance'];
                $record->field    = $field;
                $record->value    = $value;
586

587
588
589
590
591
592
593
594
595
596
597
598
599
                if ($values['create'] || !array_key_exists($field, $current)) {
                    insert_record('auth_instance_config', $record);
                }
                else {
                    update_record('auth_instance_config', $record, array('instance' => $values['instance'], 'field' => $field));
                }
            }
        }
        foreach ($configs as $config) {
            self::$default_config[$config] = get_config_plugin('auth', 'saml', $config);
        }
        return $values;
    }
600
601
602
603
604
605
606
607
608
609
610
611

    /**
     * Add "SSO Login" link below the normal login form.
     */
    public static function login_form_elements() {
        $elements = array(
            'loginsaml' => array(
                'value' => '<div class="login-externallink"><a href="' . get_config('wwwroot') . 'auth/saml/">' . get_string('login', 'auth.saml') . '</a></div>'
            )
        );
        return $elements;
    }
612
613
614
615

    public static function need_basic_login_form() {
        return false;
    }
616
}