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