Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Anobody can login with any password.
19
 *
20
 * @package auth_oauth2
21
 * @copyright 2017 Damyon Wiese
22
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
23
 */
24
 
25
namespace auth_oauth2;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use pix_icon;
30
use moodle_url;
31
use core_text;
32
use context_system;
33
use stdClass;
34
use core\oauth2\issuer;
35
use core\oauth2\client;
36
 
37
require_once($CFG->libdir.'/authlib.php');
38
require_once($CFG->dirroot.'/user/lib.php');
39
require_once($CFG->dirroot.'/user/profile/lib.php');
40
 
41
/**
42
 * Plugin for oauth2 authentication.
43
 *
44
 * @package auth_oauth2
45
 * @copyright 2017 Damyon Wiese
46
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
47
 */
48
class auth extends \auth_plugin_base {
49
 
50
    /**
51
     * @var stdClass $userinfo The set of user info returned from the oauth handshake
52
     */
53
    private static $userinfo;
54
 
55
    /**
56
     * @var stdClass $userpicture The url to a picture.
57
     */
58
    private static $userpicture;
59
 
60
    /**
61
     * Constructor.
62
     */
63
    public function __construct() {
64
        $this->authtype = 'oauth2';
65
        $this->config = get_config('auth_oauth2');
66
        $this->customfields = $this->get_custom_user_profile_fields();
67
    }
68
 
69
    /**
70
     * Returns true if the username and password work or don't exist and false
71
     * if the user exists and the password is wrong.
72
     *
73
     * @param string $username The username
74
     * @param string $password The password
75
     * @return bool Authentication success or failure.
76
     */
77
    public function user_login($username, $password) {
78
        $cached = $this->get_static_user_info();
79
        if (empty($cached)) {
80
            // This means we were called as part of a normal login flow - without using oauth.
81
            return false;
82
        }
83
        $verifyusername = $cached['username'];
84
        if ($verifyusername == $username) {
85
            return true;
86
        }
87
        return false;
88
    }
89
 
90
    /**
91
     * We don't want to allow users setting an internal password.
92
     *
93
     * @return bool
94
     */
95
    public function prevent_local_passwords() {
96
        return true;
97
    }
98
 
99
    /**
100
     * Returns true if this authentication plugin is 'internal'.
101
     *
102
     * @return bool
103
     */
104
    public function is_internal() {
105
        return false;
106
    }
107
 
108
    /**
109
     * Indicates if moodle should automatically update internal user
110
     * records with data from external sources using the information
111
     * from auth_plugin_base::get_userinfo().
112
     *
113
     * @return bool true means automatically copy data from ext to user table
114
     */
115
    public function is_synchronised_with_external() {
116
        return true;
117
    }
118
 
119
    /**
120
     * Returns true if this authentication plugin can change the user's
121
     * password.
122
     *
123
     * @return bool
124
     */
125
    public function can_change_password() {
126
        return false;
127
    }
128
 
129
    /**
130
     * Returns the URL for changing the user's pw, or empty if the default can
131
     * be used.
132
     *
133
     * @return moodle_url
134
     */
135
    public function change_password_url() {
136
        return null;
137
    }
138
 
139
    /**
140
     * Returns true if plugin allows resetting of internal password.
141
     *
142
     * @return bool
143
     */
144
    public function can_reset_password() {
145
        return false;
146
    }
147
 
148
    /**
149
     * Returns true if plugin can be manually set.
150
     *
151
     * @return bool
152
     */
153
    public function can_be_manually_set() {
154
        return true;
155
    }
156
 
157
    /**
158
     * Return the userinfo from the oauth handshake. Will only be valid
159
     * for the logged in user.
160
     * @param string $username
161
     */
162
    public function get_userinfo($username) {
163
        $cached = $this->get_static_user_info();
164
        if (!empty($cached) && $cached['username'] == $username) {
165
            return $cached;
166
        }
167
        return false;
168
    }
169
 
170
    /**
171
     * Return a list of identity providers to display on the login page.
172
     *
173
     * @param string|moodle_url $wantsurl The requested URL.
174
     * @return array List of arrays with keys url, iconurl and name.
175
     */
176
    public function loginpage_idp_list($wantsurl) {
177
        $providers = \core\oauth2\api::get_all_issuers(true);
178
        $result = [];
179
        if (empty($wantsurl)) {
180
            $wantsurl = '/';
181
        }
182
        foreach ($providers as $idp) {
183
            if ($idp->is_available_for_login()) {
184
                $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()];
185
                $url = new moodle_url('/auth/oauth2/login.php', $params);
186
                $icon = $idp->get('image');
187
                $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get_display_name()];
188
            }
189
        }
190
        return $result;
191
    }
192
 
193
    /**
194
     * Statically cache the user info from the oauth handshake
195
     * @param stdClass $userinfo
196
     */
197
    private function set_static_user_info($userinfo) {
198
        self::$userinfo = $userinfo;
199
    }
200
 
201
    /**
202
     * Get the static cached user info
203
     * @return stdClass
204
     */
205
    private function get_static_user_info() {
206
        return self::$userinfo;
207
    }
208
 
209
    /**
210
     * Statically cache the user picture from the oauth handshake
211
     * @param string $userpicture
212
     */
213
    private function set_static_user_picture($userpicture) {
214
        self::$userpicture = $userpicture;
215
    }
216
 
217
    /**
218
     * Get the static cached user picture
219
     * @return string
220
     */
221
    private function get_static_user_picture() {
222
        return self::$userpicture;
223
    }
224
 
225
    /**
226
     * If this user has no picture - but we got one from oauth - set it.
227
     * @param stdClass $user
228
     * @return boolean True if the image was updated.
229
     */
230
    private function update_picture($user) {
231
        global $CFG, $DB, $USER;
232
 
233
        require_once($CFG->libdir . '/filelib.php');
234
        require_once($CFG->libdir . '/gdlib.php');
235
        require_once($CFG->dirroot . '/user/lib.php');
236
 
237
        $fs = get_file_storage();
238
        $userid = $user->id;
239
        if (!empty($user->picture)) {
240
            return false;
241
        }
242
        if (!empty($CFG->enablegravatar)) {
243
            return false;
244
        }
245
 
246
        $picture = $this->get_static_user_picture();
247
        if (empty($picture)) {
248
            return false;
249
        }
250
 
251
        $context = \context_user::instance($userid, MUST_EXIST);
252
        $fs->delete_area_files($context->id, 'user', 'newicon');
253
 
254
        $filerecord = array(
255
            'contextid' => $context->id,
256
            'component' => 'user',
257
            'filearea' => 'newicon',
258
            'itemid' => 0,
259
            'filepath' => '/',
260
            'filename' => 'image'
261
        );
262
 
263
        try {
264
            $fs->create_file_from_string($filerecord, $picture);
265
        } catch (\file_exception $e) {
266
            return get_string($e->errorcode, $e->module, $e->a);
267
        }
268
 
269
        $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
270
 
271
        // There should only be one.
272
        $iconfile = reset($iconfile);
273
 
274
        // Something went wrong while creating temp file - remove the uploaded file.
275
        if (!$iconfile = $iconfile->copy_content_to_temp()) {
276
            $fs->delete_area_files($context->id, 'user', 'newicon');
277
            return false;
278
        }
279
 
280
        // Copy file to temporary location and the send it for processing icon.
281
        $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile);
282
        // Delete temporary file.
283
        @unlink($iconfile);
284
        // Remove uploaded file.
285
        $fs->delete_area_files($context->id, 'user', 'newicon');
286
        // Set the user's picture.
287
        $updateuser = new stdClass();
288
        $updateuser->id = $userid;
289
        $updateuser->picture = $newpicture;
290
        $USER->picture = $newpicture;
291
        user_update_user($updateuser);
292
        return true;
293
    }
294
 
295
    /**
296
     * Update user data according to data sent by authorization server.
297
     *
298
     * @param array $externaldata data from authorization server
299
     * @param stdClass $userdata Current data of the user to be updated
300
     * @return stdClass The updated user record, or the existing one if there's nothing to be updated.
301
     */
302
    private function update_user(array $externaldata, $userdata) {
303
        $user = (object) [
304
            'id' => $userdata->id,
305
        ];
306
 
307
        // We can only update if the default authentication type of the user is set to OAuth2 as well. Otherwise, we might mess
308
        // up the user data of other users that use different authentication mechanisms (e.g. linked logins).
309
        if ($userdata->auth !== $this->authtype) {
310
            return $userdata;
311
        }
312
 
313
        $allfields = array_merge($this->userfields, $this->customfields);
314
 
315
        // Go through each field from the external data.
316
        foreach ($externaldata as $fieldname => $value) {
317
            if (!in_array($fieldname, $allfields)) {
318
                // Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer.
319
                continue;
320
            }
321
 
322
            $userhasfield = property_exists($userdata, $fieldname);
323
            // Find out if it is a profile field.
324
            $isprofilefield = strpos($fieldname, 'profile_field_') === 0;
325
            $profilefieldname = str_replace('profile_field_', '', $fieldname);
326
            $userhasprofilefield = $isprofilefield && array_key_exists($profilefieldname, $userdata->profile);
327
 
328
            // Just in case this field is on the list, but not part of the user data. This shouldn't happen though.
329
            if (!($userhasfield || $userhasprofilefield)) {
330
                continue;
331
            }
332
 
333
            // Get the old value.
334
            $oldvalue = $isprofilefield ? (string) $userdata->profile[$profilefieldname] : (string) $userdata->$fieldname;
335
 
336
            // Get the lock configuration of the field.
337
            if (!empty($this->config->{'field_lock_' . $fieldname})) {
338
                $lockvalue = $this->config->{'field_lock_' . $fieldname};
339
            } else {
340
                $lockvalue = 'unlocked';
341
            }
342
 
343
            // We should update fields that meet the following criteria:
344
            // - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty.
345
            // - The value has changed.
346
            if ($lockvalue === 'unlocked' || ($lockvalue === 'unlockedifempty' && empty($oldvalue))) {
347
                $value = (string)$value;
348
                if ($oldvalue !== $value) {
349
                    $user->$fieldname = $value;
350
                }
351
            }
352
        }
353
        // Update the user data.
354
        user_update_user($user, false);
355
 
356
        // Save user profile data.
357
        profile_save_data($user);
358
 
359
        // Refresh user for $USER variable.
360
        return get_complete_user_data('id', $user->id);
361
    }
362
 
363
    /**
364
     * Confirm the new user as registered.
365
     *
366
     * @param string $username
367
     * @param string $confirmsecret
368
     */
369
    public function user_confirm($username, $confirmsecret) {
370
        global $DB;
371
        $user = get_complete_user_data('username', $username);
372
 
373
        if (!empty($user)) {
374
            if ($user->auth != $this->authtype) {
375
                return AUTH_CONFIRM_ERROR;
376
 
377
            } else if ($user->secret === $confirmsecret && $user->confirmed) {
378
                return AUTH_CONFIRM_ALREADY;
379
 
380
            } else if ($user->secret === $confirmsecret) {   // They have provided the secret key to get in.
381
                $DB->set_field("user", "confirmed", 1, array("id" => $user->id));
382
                return AUTH_CONFIRM_OK;
383
            }
384
        } else {
385
            return AUTH_CONFIRM_ERROR;
386
        }
387
    }
388
 
389
    /**
390
     * Print a page showing that a confirm email was sent with instructions.
391
     *
392
     * @param string $title
393
     * @param string $message
394
     */
395
    public function print_confirm_required($title, $message) {
396
        global $PAGE, $OUTPUT, $CFG;
397
 
398
        $PAGE->navbar->add($title);
399
        $PAGE->set_title($title);
400
        $PAGE->set_heading($PAGE->course->fullname);
401
        echo $OUTPUT->header();
402
        notice($message, "$CFG->wwwroot/index.php");
403
    }
404
 
405
    /**
406
     * Complete the login process after oauth handshake is complete.
407
     * @param \core\oauth2\client $client
408
     * @param string $redirecturl
409
     * @return void Either redirects or throws an exception
410
     */
411
    public function complete_login(client $client, $redirecturl) {
412
        global $CFG, $SESSION, $PAGE;
413
 
414
        $rawuserinfo = $client->get_raw_userinfo();
415
        $userinfo = $client->get_userinfo();
416
 
417
        if (!$userinfo) {
418
            // Trigger login failed event.
419
            $failurereason = AUTH_LOGIN_NOUSER;
420
            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
421
                                                                        'reason' => $failurereason]]);
422
            $event->trigger();
423
 
424
            $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2');
425
            $SESSION->loginerrormsg = $errormsg;
426
            $client->log_out();
427
            redirect(new moodle_url('/login/index.php'));
428
        }
429
        if (empty($userinfo['username']) || empty($userinfo['email'])) {
430
            // Trigger login failed event.
431
            $failurereason = AUTH_LOGIN_NOUSER;
432
            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
433
                                                                        'reason' => $failurereason]]);
434
            $event->trigger();
435
 
436
            $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2');
437
            $SESSION->loginerrormsg = $errormsg;
438
            $client->log_out();
439
            redirect(new moodle_url('/login/index.php'));
440
        }
441
 
442
        $userinfo['username'] = trim(core_text::strtolower($userinfo['username']));
443
        $oauthemail = $userinfo['email'];
444
 
445
        // Once we get here we have the user info from oauth.
446
        $userwasmapped = false;
447
 
448
        // Clean and remember the picture / lang.
449
        if (!empty($userinfo['picture'])) {
450
            $this->set_static_user_picture($userinfo['picture']);
451
            unset($userinfo['picture']);
452
        }
453
 
454
        if (!empty($userinfo['lang'])) {
455
            $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang'])));
456
            if (!get_string_manager()->translation_exists($userinfo['lang'], false)) {
457
                unset($userinfo['lang']);
458
            }
459
        }
460
 
461
        $issuer = $client->get_issuer();
462
        // First we try and find a defined mapping.
463
        $linkedlogin = api::match_username_to_user($userinfo['username'], $issuer);
464
 
465
        if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) {
466
            $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid'));
467
 
468
            if ($mappeduser && $mappeduser->suspended) {
469
                $failurereason = AUTH_LOGIN_SUSPENDED;
470
                $event = \core\event\user_login_failed::create([
471
                    'userid' => $mappeduser->id,
472
                    'other' => [
473
                        'username' => $userinfo['username'],
474
                        'reason' => $failurereason
475
                    ]
476
                ]);
477
                $event->trigger();
478
                $SESSION->loginerrormsg = get_string('invalidlogin');
479
                $client->log_out();
480
                redirect(new moodle_url('/login/index.php'));
481
            } else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) {
482
                // Update user fields.
483
                $userinfo = $this->update_user($userinfo, $mappeduser);
484
                $userwasmapped = true;
485
            } else {
486
                // Trigger login failed event.
487
                $failurereason = AUTH_LOGIN_UNAUTHORISED;
488
                $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
489
                                                                            'reason' => $failurereason]]);
490
                $event->trigger();
491
 
492
                $errormsg = get_string('confirmationpending', 'auth_oauth2');
493
                $SESSION->loginerrormsg = $errormsg;
494
                $client->log_out();
495
                redirect(new moodle_url('/login/index.php'));
496
            }
497
        } else if (!empty($linkedlogin)) {
498
            // Trigger login failed event.
499
            $failurereason = AUTH_LOGIN_UNAUTHORISED;
500
            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
501
                                                                        'reason' => $failurereason]]);
502
            $event->trigger();
503
 
504
            $errormsg = get_string('confirmationpending', 'auth_oauth2');
505
            $SESSION->loginerrormsg = $errormsg;
506
            $client->log_out();
507
            redirect(new moodle_url('/login/index.php'));
508
        }
509
 
510
 
511
        if (!$issuer->is_valid_login_domain($oauthemail)) {
512
            // Trigger login failed event.
513
            $failurereason = AUTH_LOGIN_UNAUTHORISED;
514
            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
515
                                                                        'reason' => $failurereason]]);
516
            $event->trigger();
517
 
518
            $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2'));
519
            $SESSION->loginerrormsg = $errormsg;
520
            $client->log_out();
521
            redirect(new moodle_url('/login/index.php'));
522
        }
523
 
524
        if (!$userwasmapped) {
525
            // No defined mapping - we need to see if there is an existing account with the same email.
526
 
527
            $moodleuser = \core_user::get_user_by_email($userinfo['email']);
528
            if (!empty($moodleuser)) {
529
                if ($issuer->get('requireconfirmation')) {
530
                    $PAGE->set_url('/auth/oauth2/confirm-link-login.php');
531
                    $PAGE->set_context(context_system::instance());
532
 
533
                    \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id);
534
                    // Request to link to existing account.
535
                    $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2');
536
                    $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email);
537
                    $this->print_confirm_required($emailconfirm, $message);
538
                    exit();
539
                } else {
540
                    \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true);
541
                    // We dont have profile loaded on $moodleuser, so load it.
542
                    require_once($CFG->dirroot.'/user/profile/lib.php');
543
                    profile_load_custom_fields($moodleuser);
544
                    $userinfo = $this->update_user($userinfo, $moodleuser);
545
                    // No redirect, we will complete this login.
546
                }
547
 
548
            } else {
549
                // This is a new account.
550
                $exists = \core_user::get_user_by_username($userinfo['username']);
551
                // Creating a new user?
552
                if ($exists) {
553
                    // Trigger login failed event.
554
                    $failurereason = AUTH_LOGIN_FAILED;
555
                    $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
556
                                                                                'reason' => $failurereason]]);
557
                    $event->trigger();
558
 
559
                    // The username exists but the emails don't match. Refuse to continue.
560
                    $errormsg = get_string('accountexists', 'auth_oauth2');
561
                    $SESSION->loginerrormsg = $errormsg;
562
                    $client->log_out();
563
                    redirect(new moodle_url('/login/index.php'));
564
                }
565
 
566
                if (email_is_not_allowed($userinfo['email'])) {
567
                    // Trigger login failed event.
568
                    $failurereason = AUTH_LOGIN_FAILED;
569
                    $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
570
                                                                                'reason' => $failurereason]]);
571
                    $event->trigger();
572
                    // The username exists but the emails don't match. Refuse to continue.
573
                    $reason = get_string('loginerror_invaliddomain', 'auth_oauth2');
574
                    $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason);
575
                    $SESSION->loginerrormsg = $errormsg;
576
                    $client->log_out();
577
                    redirect(new moodle_url('/login/index.php'));
578
                }
579
 
580
                if (!empty($CFG->authpreventaccountcreation)) {
581
                    // Trigger login failed event.
582
                    $failurereason = AUTH_LOGIN_UNAUTHORISED;
583
                    $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
584
                                                                                'reason' => $failurereason]]);
585
                    $event->trigger();
586
                    // The username does not exist and settings prevent creating new accounts.
587
                    $reason = get_string('loginerror_cannotcreateaccounts', 'auth_oauth2');
588
                    $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason);
589
                    $SESSION->loginerrormsg = $errormsg;
590
                    $client->log_out();
591
                    redirect(new moodle_url('/login/index.php'));
592
                }
593
 
594
                if ($issuer->get('requireconfirmation')) {
595
                    $PAGE->set_url('/auth/oauth2/confirm-account.php');
596
                    $PAGE->set_context(context_system::instance());
597
 
598
                    // Create a new (unconfirmed account) and send an email to confirm it.
599
                    $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer);
600
 
601
                    $this->update_picture($user);
602
                    $emailconfirm = get_string('emailconfirm');
603
                    $message = get_string('emailconfirmsent', '', $userinfo['email']);
604
                    $this->print_confirm_required($emailconfirm, $message);
605
                    exit();
606
                } else {
607
                    // Create a new confirmed account.
608
                    $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer);
609
                    $userinfo = get_complete_user_data('id', $newuser->id);
610
                    // No redirect, we will complete this login.
611
                }
612
            }
613
        }
614
 
615
        // We used to call authenticate_user - but that won't work if the current user has a different default authentication
616
        // method. Since we now ALWAYS link a login - if we get to here we can directly allow the user in.
617
        $user = (object) $userinfo;
618
 
619
        // Add extra loggedin info.
620
        $this->set_extrauserinfo((array)$rawuserinfo);
621
 
622
        complete_user_login($user, $this->get_extrauserinfo());
623
        $this->update_picture($user);
624
        redirect($redirecturl);
625
    }
626
 
627
    /**
628
     * Returns information on how the specified user can change their password.
629
     * The password of the oauth2 accounts is not stored in Moodle.
630
     *
631
     * @param stdClass $user A user object
632
     * @return string[] An array of strings with keys subject and message
633
     */
634
    public function get_password_change_info(stdClass $user): array {
635
        $site = get_site();
636
 
637
        $data = new stdClass();
638
        $data->firstname = $user->firstname;
639
        $data->lastname  = $user->lastname;
640
        $data->username  = $user->username;
641
        $data->sitename  = format_string($site->fullname);
642
        $data->admin     = generate_email_signoff();
643
 
644
        $message = get_string('emailpasswordchangeinfo', 'auth_oauth2', $data);
645
        $subject = get_string('emailpasswordchangeinfosubject', 'auth_oauth2', format_string($site->fullname));
646
 
647
        return [
648
            'subject' => $subject,
649
            'message' => $message
650
        ];
651
    }
652
}