Merge branch 'MDL-58898-master' of https://github.com/xow/moodle
[moodle.git] / auth / oauth2 / classes / auth.php
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/>.
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  */
25 namespace auth_oauth2;
27 defined('MOODLE_INTERNAL') || die();
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;
37 require_once($CFG->libdir.'/authlib.php');
39 /**
40  * Plugin for oauth2 authentication.
41  *
42  * @package auth_oauth2
43  * @copyright 2017 Damyon Wiese
44  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
45  */
46 class auth extends \auth_plugin_base {
48     /**
49      * @var stdClass $userinfo The set of user info returned from the oauth handshake
50      */
51     private static $userinfo;
53     /**
54      * @var stdClass $userpicture The url to a picture.
55      */
56     private static $userpicture;
58     /**
59      * Constructor.
60      */
61     public function __construct() {
62         $this->authtype = 'oauth2';
63         $this->config = get_config('auth_oauth2');
64     }
66     /**
67      * Returns true if the username and password work or don't exist and false
68      * if the user exists and the password is wrong.
69      *
70      * @param string $username The username
71      * @param string $password The password
72      * @return bool Authentication success or failure.
73      */
74     public function user_login($username, $password) {
75         $cached = $this->get_static_user_info();
76         if (empty($cached)) {
77             // This means we were called as part of a normal login flow - without using oauth.
78             return false;
79         }
80         $verifyusername = $cached['username'];
81         if ($verifyusername == $username) {
82             return true;
83         }
84         return false;
85     }
87     /**
88      * We don't want to allow users setting an internal password.
89      *
90      * @return bool
91      */
92     public function prevent_local_passwords() {
93         return true;
94     }
96     /**
97      * Returns true if this authentication plugin is 'internal'.
98      *
99      * @return bool
100      */
101     public function is_internal() {
102         return false;
103     }
105     /**
106      * Indicates if moodle should automatically update internal user
107      * records with data from external sources using the information
108      * from auth_plugin_base::get_userinfo().
109      *
110      * @return bool true means automatically copy data from ext to user table
111      */
112     public function is_synchronised_with_external() {
113         return false;
114     }
116     /**
117      * Returns true if this authentication plugin can change the user's
118      * password.
119      *
120      * @return bool
121      */
122     public function can_change_password() {
123         return false;
124     }
126     /**
127      * Returns the URL for changing the user's pw, or empty if the default can
128      * be used.
129      *
130      * @return moodle_url
131      */
132     public function change_password_url() {
133         return null;
134     }
136     /**
137      * Returns true if plugin allows resetting of internal password.
138      *
139      * @return bool
140      */
141     public function can_reset_password() {
142         return false;
143     }
145     /**
146      * Returns true if plugin can be manually set.
147      *
148      * @return bool
149      */
150     public function can_be_manually_set() {
151         return true;
152     }
154     /**
155      * Return the userinfo from the oauth handshake. Will only be valid
156      * for the logged in user.
157      * @param string $username
158      */
159     public function get_userinfo($username) {
160         $cached = $this->get_static_user_info();
161         if (!empty($cached) && $cached['username'] == $username) {
162             return $cached;
163         }
164         return false;
165     }
167     /**
168      * Do some checks on the identity provider before showing it on the login page.
169      * @param core\oauth2\issuer $issuer
170      * @return boolean
171      */
172     private function is_ready_for_login_page(\core\oauth2\issuer $issuer) {
173         return $issuer->get('enabled') &&
174                 $issuer->is_configured() &&
175                 !empty($issuer->get('showonloginpage'));
176     }
178     /**
179      * Return a list of identity providers to display on the login page.
180      *
181      * @param string|moodle_url $wantsurl The requested URL.
182      * @return array List of arrays with keys url, iconurl and name.
183      */
184     public function loginpage_idp_list($wantsurl) {
185         $providers = \core\oauth2\api::get_all_issuers();
186         $result = [];
187         if (empty($wantsurl)) {
188             $wantsurl = '/';
189         }
190         foreach ($providers as $idp) {
191             if ($this->is_ready_for_login_page($idp)) {
192                 $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()];
193                 $url = new moodle_url('/auth/oauth2/login.php', $params);
194                 $icon = $idp->get('image');
195                 $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get('name')];
196             }
197         }
198         return $result;
199     }
201     /**
202      * Statically cache the user info from the oauth handshake
203      * @param stdClass $userinfo
204      */
205     private function set_static_user_info($userinfo) {
206         self::$userinfo = $userinfo;
207     }
209     /**
210      * Get the static cached user info
211      * @return stdClass
212      */
213     private function get_static_user_info() {
214         return self::$userinfo;
215     }
217     /**
218      * Statically cache the user picture from the oauth handshake
219      * @param string $userpicture
220      */
221     private function set_static_user_picture($userpicture) {
222         self::$userpicture = $userpicture;
223     }
225     /**
226      * Get the static cached user picture
227      * @return string
228      */
229     private function get_static_user_picture() {
230         return self::$userpicture;
231     }
233     /**
234      * If this user has no picture - but we got one from oauth - set it.
235      * @param stdClass $user
236      * @return boolean True if the image was updated.
237      */
238     private function update_picture($user) {
239         global $CFG, $DB, $USER;
241         require_once($CFG->libdir . '/filelib.php');
242         require_once($CFG->libdir . '/gdlib.php');
243         require_once($CFG->dirroot . '/user/lib.php');
245         $fs = get_file_storage();
246         $userid = $user->id;
247         if (!empty($user->picture)) {
248             return false;
249         }
250         $picture = $this->get_static_user_picture();
251         if (empty($picture)) {
252             return false;
253         }
255         $context = \context_user::instance($userid, MUST_EXIST);
256         $fs->delete_area_files($context->id, 'user', 'newicon');
258         $filerecord = array(
259             'contextid' => $context->id,
260             'component' => 'user',
261             'filearea' => 'newicon',
262             'itemid' => 0,
263             'filepath' => '/',
264             'filename' => 'image'
265         );
267         try {
268             $fs->create_file_from_string($filerecord, $picture);
269         } catch (\file_exception $e) {
270             return get_string($e->errorcode, $e->module, $e->a);
271         }
273         $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
275         // There should only be one.
276         $iconfile = reset($iconfile);
278         // Something went wrong while creating temp file - remove the uploaded file.
279         if (!$iconfile = $iconfile->copy_content_to_temp()) {
280             $fs->delete_area_files($context->id, 'user', 'newicon');
281             return false;
282         }
284         // Copy file to temporary location and the send it for processing icon.
285         $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile);
286         // Delete temporary file.
287         @unlink($iconfile);
288         // Remove uploaded file.
289         $fs->delete_area_files($context->id, 'user', 'newicon');
290         // Set the user's picture.
291         $updateuser = new stdClass();
292         $updateuser->id = $userid;
293         $updateuser->picture = $newpicture;
294         $USER->picture = $newpicture;
295         user_update_user($updateuser);
296         return true;
297     }
299     /**
300      * Confirm the new user as registered.
301      *
302      * @param string $username
303      * @param string $confirmsecret
304      */
305     public function user_confirm($username, $confirmsecret) {
306         global $DB;
307         $user = get_complete_user_data('username', $username);
309         if (!empty($user)) {
310             if ($user->auth != $this->authtype) {
311                 return AUTH_CONFIRM_ERROR;
313             } else if ($user->secret == $confirmsecret && $user->confirmed) {
314                 return AUTH_CONFIRM_ALREADY;
316             } else if ($user->secret == $confirmsecret) {   // They have provided the secret key to get in.
317                 $DB->set_field("user", "confirmed", 1, array("id" => $user->id));
318                 return AUTH_CONFIRM_OK;
319             }
320         } else {
321             return AUTH_CONFIRM_ERROR;
322         }
323     }
325     /**
326      * Print a page showing that a confirm email was sent with instructions.
327      *
328      * @param string $title
329      * @param string $message
330      */
331     public function print_confirm_required($title, $message) {
332         global $PAGE, $OUTPUT, $CFG;
334         $PAGE->navbar->add($title);
335         $PAGE->set_title($title);
336         $PAGE->set_heading($PAGE->course->fullname);
337         echo $OUTPUT->header();
338         notice($message, "$CFG->httpswwwroot/index.php");
339     }
341     /**
342      * Complete the login process after oauth handshake is complete.
343      * @param \core\oauth2\client $client
344      * @param string $redirecturl
345      * @return none Either redirects or throws an exception
346      */
347     public function complete_login(client $client, $redirecturl) {
348         global $CFG, $SESSION, $PAGE;
350         $userinfo = $client->get_userinfo();
352         if (!$userinfo) {
353             // Trigger login failed event.
354             $failurereason = AUTH_LOGIN_NOUSER;
355             $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
356                                                                         'reason' => $failurereason]]);
357             $event->trigger();
359             $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2');
360             $SESSION->loginerrormsg = $errormsg;
361             $client->log_out();
362             redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
363         }
364         if (empty($userinfo['username']) || empty($userinfo['email'])) {
365             // Trigger login failed event.
366             $failurereason = AUTH_LOGIN_NOUSER;
367             $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
368                                                                         'reason' => $failurereason]]);
369             $event->trigger();
371             $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2');
372             $SESSION->loginerrormsg = $errormsg;
373             $client->log_out();
374             redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
375         }
377         $userinfo['username'] = trim(core_text::strtolower($userinfo['username']));
378         $oauthemail = $userinfo['email'];
380         // Once we get here we have the user info from oauth.
381         $userwasmapped = false;
383         // Clean and remember the picture / lang.
384         if (!empty($userinfo['picture'])) {
385             $this->set_static_user_picture($userinfo['picture']);
386             unset($userinfo['picture']);
387         }
389         if (!empty($userinfo['lang'])) {
390             $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang'])));
391             if (!get_string_manager()->translation_exists($userinfo['lang'], false)) {
392                 unset($userinfo['lang']);
393             }
394         }
396         // First we try and find a defined mapping.
397         $linkedlogin = api::match_username_to_user($userinfo['username'], $client->get_issuer());
399         if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) {
400             $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid'));
402             if ($mappeduser && $mappeduser->confirmed) {
403                 $userinfo = (array) $mappeduser;
404                 $userwasmapped = true;
405             } else {
406                 // Trigger login failed event.
407                 $failurereason = AUTH_LOGIN_UNAUTHORISED;
408                 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
409                                                                             'reason' => $failurereason]]);
410                 $event->trigger();
412                 $errormsg = get_string('confirmationpending', 'auth_oauth2');
413                 $SESSION->loginerrormsg = $errormsg;
414                 $client->log_out();
415                 redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
416             }
417         } else if (!empty($linkedlogin)) {
418             // Trigger login failed event.
419             $failurereason = AUTH_LOGIN_UNAUTHORISED;
420             $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
421                                                                         'reason' => $failurereason]]);
422             $event->trigger();
424             $errormsg = get_string('confirmationpending', 'auth_oauth2');
425             $SESSION->loginerrormsg = $errormsg;
426             $client->log_out();
427             redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
428         }
430         $issuer = $client->get_issuer();
431         if (!$issuer->is_valid_login_domain($oauthemail)) {
432             // Trigger login failed event.
433             $failurereason = AUTH_LOGIN_UNAUTHORISED;
434             $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
435                                                                         'reason' => $failurereason]]);
436             $event->trigger();
438             $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2'));
439             $SESSION->loginerrormsg = $errormsg;
440             $client->log_out();
441             redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
442         }
444         if (!$userwasmapped) {
445             // No defined mapping - we need to see if there is an existing account with the same email.
447             $moodleuser = \core_user::get_user_by_email($userinfo['email']);
448             if (!empty($moodleuser)) {
449                 $PAGE->set_url('/auth/oauth2/confirm-link-login.php');
450                 $PAGE->set_context(context_system::instance());
452                 \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id);
453                 // Request to link to existing account.
454                 $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2');
455                 $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email);
456                 $this->print_confirm_required($emailconfirm, $message);
457                 exit();
459             } else {
460                 // This is a new account.
461                 $exists = \core_user::get_user_by_username($userinfo['username']);
462                 // Creating a new user?
463                 if ($exists) {
464                     // Trigger login failed event.
465                     $failurereason = AUTH_LOGIN_FAILED;
466                     $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
467                                                                                 'reason' => $failurereason]]);
468                     $event->trigger();
470                     // The username exists but the emails don't match. Refuse to continue.
471                     $errormsg = get_string('accountexists', 'auth_oauth2');
472                     $SESSION->loginerrormsg = $errormsg;
473                     $client->log_out();
474                     redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
475                 }
477                 if (email_is_not_allowed($userinfo['email'])) {
478                     // Trigger login failed event.
479                     $failurereason = AUTH_LOGIN_FAILED;
480                     $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
481                                                                                 'reason' => $failurereason]]);
482                     $event->trigger();
483                     // The username exists but the emails don't match. Refuse to continue.
484                     $reason = get_string('loginerror_invaliddomain', 'auth_oauth2');
485                     $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason);
486                     $SESSION->loginerrormsg = $errormsg;
487                     $client->log_out();
488                     redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
489                 }
491                 if (!empty($CFG->authpreventaccountcreation)) {
492                     // Trigger login failed event.
493                     $failurereason = AUTH_LOGIN_UNAUTHORISED;
494                     $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
495                                                                                 'reason' => $failurereason]]);
496                     $event->trigger();
497                     // The username does not exist and settings prevent creating new accounts.
498                     $reason = get_string('loginerror_cannotcreateaccounts', 'auth_oauth2');
499                     $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason);
500                     $SESSION->loginerrormsg = $errormsg;
501                     $client->log_out();
502                     redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
503                 }
505                 $PAGE->set_url('/auth/oauth2/confirm-account.php');
506                 $PAGE->set_context(context_system::instance());
508                 // Create a new (unconfirmed account) and send an email to confirm it.
509                 $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer);
511                 $this->update_picture($user);
512                 $emailconfirm = get_string('emailconfirm');
513                 $message = get_string('emailconfirmsent', '', $userinfo['email']);
514                 $this->print_confirm_required($emailconfirm, $message);
515                 exit();
516             }
517         }
519         // We used to call authenticate_user - but that won't work if the current user has a different default authentication
520         // method. Since we now ALWAYS link a login - if we get to here we can directly allow the user in.
521         $user = (object) $userinfo;
522         complete_user_login($user);
523         $this->update_picture($user);
524         redirect($redirecturl);
525     }