Merge branch 'm35_MDL-61626_PgSQL_Table_Name_Could_Be_Schema_Qualified' of https...
[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');
38
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 */
46class auth extends \auth_plugin_base {
47
48 /**
49 * @var stdClass $userinfo The set of user info returned from the oauth handshake
50 */
51 private static $userinfo;
52
53 /**
54 * @var stdClass $userpicture The url to a picture.
55 */
56 private static $userpicture;
57
58 /**
59 * Constructor.
60 */
61 public function __construct() {
62 $this->authtype = 'oauth2';
63 $this->config = get_config('auth_oauth2');
64 }
65
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();
bfc60d38
DW
76 if (empty($cached)) {
77 // This means we were called as part of a normal login flow - without using oauth.
78 return false;
79 }
60237253
DW
80 $verifyusername = $cached['username'];
81 if ($verifyusername == $username) {
82 return true;
83 }
979d1f66 84 return false;
60237253
DW
85 }
86
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 }
95
96 /**
97 * Returns true if this authentication plugin is 'internal'.
98 *
99 * @return bool
100 */
101 public function is_internal() {
102 return false;
103 }
104
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() {
28b592d5 113 return false;
60237253
DW
114 }
115
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 }
125
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 }
135
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 }
144
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 }
153
60237253
DW
154 /**
155 * Return the userinfo from the oauth handshake. Will only be valid
156 * for the logged in user.
72fd103a 157 * @param string $username
60237253
DW
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 }
166
f9f243f9
DW
167 /**
168 * Do some checks on the identity provider before showing it on the login page.
29911249 169 * @param core\oauth2\issuer $issuer
f9f243f9
DW
170 * @return boolean
171 */
172 private function is_ready_for_login_page(\core\oauth2\issuer $issuer) {
eca128bf 173 return $issuer->get('enabled') &&
5b0b35c0 174 $issuer->is_configured() &&
60237253
DW
175 !empty($issuer->get('showonloginpage'));
176 }
177
178 /**
179 * Return a list of identity providers to display on the login page.
f9f243f9
DW
180 *
181 * @param string|moodle_url $wantsurl The requested URL.
1cb5c7b3 182 * @return array List of arrays with keys url, iconurl and name.
60237253
DW
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 }
200
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 }
208
209 /**
210 * Get the static cached user info
211 * @return stdClass
212 */
213 private function get_static_user_info() {
214 return self::$userinfo;
215 }
216
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 }
224
225 /**
226 * Get the static cached user picture
227 * @return string
228 */
229 private function get_static_user_picture() {
230 return self::$userpicture;
231 }
232
233 /**
234 * If this user has no picture - but we got one from oauth - set it.
29911249 235 * @param stdClass $user
60237253
DW
236 * @return boolean True if the image was updated.
237 */
238 private function update_picture($user) {
239 global $CFG, $DB, $USER;
240
241 require_once($CFG->libdir . '/filelib.php');
242 require_once($CFG->libdir . '/gdlib.php');
427fa27c 243 require_once($CFG->dirroot . '/user/lib.php');
60237253
DW
244
245 $fs = get_file_storage();
246 $userid = $user->id;
247 if (!empty($user->picture)) {
248 return false;
249 }
95e13556
DW
250 if (!empty($CFG->enablegravatar)) {
251 return false;
252 }
253
60237253
DW
254 $picture = $this->get_static_user_picture();
255 if (empty($picture)) {
256 return false;
257 }
258
259 $context = \context_user::instance($userid, MUST_EXIST);
260 $fs->delete_area_files($context->id, 'user', 'newicon');
261
262 $filerecord = array(
263 'contextid' => $context->id,
264 'component' => 'user',
265 'filearea' => 'newicon',
266 'itemid' => 0,
267 'filepath' => '/',
268 'filename' => 'image'
269 );
270
271 try {
272 $fs->create_file_from_string($filerecord, $picture);
273 } catch (\file_exception $e) {
274 return get_string($e->errorcode, $e->module, $e->a);
275 }
276
277 $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
278
279 // There should only be one.
280 $iconfile = reset($iconfile);
281
282 // Something went wrong while creating temp file - remove the uploaded file.
283 if (!$iconfile = $iconfile->copy_content_to_temp()) {
284 $fs->delete_area_files($context->id, 'user', 'newicon');
285 return false;
286 }
287
288 // Copy file to temporary location and the send it for processing icon.
289 $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile);
290 // Delete temporary file.
291 @unlink($iconfile);
292 // Remove uploaded file.
293 $fs->delete_area_files($context->id, 'user', 'newicon');
294 // Set the user's picture.
295 $updateuser = new stdClass();
296 $updateuser->id = $userid;
297 $updateuser->picture = $newpicture;
298 $USER->picture = $newpicture;
299 user_update_user($updateuser);
300 return true;
301 }
302
72fd103a 303 /**
28b592d5
DW
304 * Confirm the new user as registered.
305 *
306 * @param string $username
307 * @param string $confirmsecret
72fd103a 308 */
13b449f4 309 public function user_confirm($username, $confirmsecret) {
28b592d5
DW
310 global $DB;
311 $user = get_complete_user_data('username', $username);
312
313 if (!empty($user)) {
314 if ($user->auth != $this->authtype) {
315 return AUTH_CONFIRM_ERROR;
316
317 } else if ($user->secret == $confirmsecret && $user->confirmed) {
318 return AUTH_CONFIRM_ALREADY;
319
13b449f4
DW
320 } else if ($user->secret == $confirmsecret) { // They have provided the secret key to get in.
321 $DB->set_field("user", "confirmed", 1, array("id" => $user->id));
28b592d5
DW
322 return AUTH_CONFIRM_OK;
323 }
324 } else {
325 return AUTH_CONFIRM_ERROR;
1dca8d1a 326 }
28b592d5
DW
327 }
328
329 /**
330 * Print a page showing that a confirm email was sent with instructions.
331 *
13b449f4
DW
332 * @param string $title
333 * @param string $message
28b592d5
DW
334 */
335 public function print_confirm_required($title, $message) {
336 global $PAGE, $OUTPUT, $CFG;
337
338 $PAGE->navbar->add($title);
339 $PAGE->set_title($title);
340 $PAGE->set_heading($PAGE->course->fullname);
341 echo $OUTPUT->header();
672f4836 342 notice($message, "$CFG->wwwroot/index.php");
1dca8d1a
DW
343 }
344
60237253
DW
345 /**
346 * Complete the login process after oauth handshake is complete.
347 * @param \core\oauth2\client $client
348 * @param string $redirecturl
92c8cb9d 349 * @return void Either redirects or throws an exception
60237253
DW
350 */
351 public function complete_login(client $client, $redirecturl) {
28b592d5 352 global $CFG, $SESSION, $PAGE;
60237253
DW
353
354 $userinfo = $client->get_userinfo();
355
356 if (!$userinfo) {
6cee96c8
DW
357 // Trigger login failed event.
358 $failurereason = AUTH_LOGIN_NOUSER;
92c8cb9d 359 $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
6cee96c8
DW
360 'reason' => $failurereason]]);
361 $event->trigger();
362
7f158660 363 $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2');
60237253 364 $SESSION->loginerrormsg = $errormsg;
427fa27c 365 $client->log_out();
7eb50b32 366 redirect(new moodle_url('/login/index.php'));
8445556b 367 }
28b592d5 368 if (empty($userinfo['username']) || empty($userinfo['email'])) {
6cee96c8
DW
369 // Trigger login failed event.
370 $failurereason = AUTH_LOGIN_NOUSER;
92c8cb9d 371 $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
6cee96c8
DW
372 'reason' => $failurereason]]);
373 $event->trigger();
374
7f158660 375 $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2');
8445556b 376 $SESSION->loginerrormsg = $errormsg;
427fa27c 377 $client->log_out();
7eb50b32 378 redirect(new moodle_url('/login/index.php'));
60237253
DW
379 }
380
381 $userinfo['username'] = trim(core_text::strtolower($userinfo['username']));
427fa27c 382 $oauthemail = $userinfo['email'];
60237253 383
28b592d5 384 // Once we get here we have the user info from oauth.
1dca8d1a 385 $userwasmapped = false;
1dca8d1a 386
28b592d5
DW
387 // Clean and remember the picture / lang.
388 if (!empty($userinfo['picture'])) {
389 $this->set_static_user_picture($userinfo['picture']);
390 unset($userinfo['picture']);
391 }
392
393 if (!empty($userinfo['lang'])) {
394 $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang'])));
395 if (!get_string_manager()->translation_exists($userinfo['lang'], false)) {
396 unset($userinfo['lang']);
397 }
398 }
399
400 // First we try and find a defined mapping.
401 $linkedlogin = api::match_username_to_user($userinfo['username'], $client->get_issuer());
402
403 if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) {
404 $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid'));
405
406 if ($mappeduser && $mappeduser->confirmed) {
1dca8d1a
DW
407 $userinfo = (array) $mappeduser;
408 $userwasmapped = true;
28b592d5 409 } else {
6cee96c8
DW
410 // Trigger login failed event.
411 $failurereason = AUTH_LOGIN_UNAUTHORISED;
412 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
413 'reason' => $failurereason]]);
414 $event->trigger();
415
28b592d5
DW
416 $errormsg = get_string('confirmationpending', 'auth_oauth2');
417 $SESSION->loginerrormsg = $errormsg;
427fa27c 418 $client->log_out();
7eb50b32 419 redirect(new moodle_url('/login/index.php'));
1dca8d1a 420 }
28b592d5 421 } else if (!empty($linkedlogin)) {
6cee96c8
DW
422 // Trigger login failed event.
423 $failurereason = AUTH_LOGIN_UNAUTHORISED;
424 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
425 'reason' => $failurereason]]);
426 $event->trigger();
427
28b592d5
DW
428 $errormsg = get_string('confirmationpending', 'auth_oauth2');
429 $SESSION->loginerrormsg = $errormsg;
427fa27c 430 $client->log_out();
7eb50b32 431 redirect(new moodle_url('/login/index.php'));
28b592d5 432 }
427fa27c 433
28b592d5 434 $issuer = $client->get_issuer();
427fa27c 435 if (!$issuer->is_valid_login_domain($oauthemail)) {
6cee96c8
DW
436 // Trigger login failed event.
437 $failurereason = AUTH_LOGIN_UNAUTHORISED;
438 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
439 'reason' => $failurereason]]);
440 $event->trigger();
441
7f158660 442 $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2'));
28b592d5 443 $SESSION->loginerrormsg = $errormsg;
427fa27c 444 $client->log_out();
7eb50b32 445 redirect(new moodle_url('/login/index.php'));
60237253
DW
446 }
447
1dca8d1a 448 if (!$userwasmapped) {
28b592d5
DW
449 // No defined mapping - we need to see if there is an existing account with the same email.
450
451 $moodleuser = \core_user::get_user_by_email($userinfo['email']);
452 if (!empty($moodleuser)) {
859e2033
DW
453 if ($issuer->get('requireconfirmation')) {
454 $PAGE->set_url('/auth/oauth2/confirm-link-login.php');
455 $PAGE->set_context(context_system::instance());
456
457 \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id);
458 // Request to link to existing account.
459 $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2');
460 $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email);
461 $this->print_confirm_required($emailconfirm, $message);
462 exit();
463 } else {
464 \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true);
465 $userinfo = get_complete_user_data('id', $moodleuser->id);
466 // No redirect, we will complete this login.
467 }
28b592d5
DW
468
469 } else {
470 // This is a new account.
471 $exists = \core_user::get_user_by_username($userinfo['username']);
472 // Creating a new user?
473 if ($exists) {
6cee96c8
DW
474 // Trigger login failed event.
475 $failurereason = AUTH_LOGIN_FAILED;
476 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
477 'reason' => $failurereason]]);
478 $event->trigger();
28b592d5
DW
479
480 // The username exists but the emails don't match. Refuse to continue.
481 $errormsg = get_string('accountexists', 'auth_oauth2');
482 $SESSION->loginerrormsg = $errormsg;
427fa27c 483 $client->log_out();
7eb50b32 484 redirect(new moodle_url('/login/index.php'));
28b592d5 485 }
1dca8d1a 486
28b592d5 487 if (email_is_not_allowed($userinfo['email'])) {
6cee96c8
DW
488 // Trigger login failed event.
489 $failurereason = AUTH_LOGIN_FAILED;
490 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
491 'reason' => $failurereason]]);
492 $event->trigger();
28b592d5 493 // The username exists but the emails don't match. Refuse to continue.
7f158660
DW
494 $reason = get_string('loginerror_invaliddomain', 'auth_oauth2');
495 $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason);
28b592d5 496 $SESSION->loginerrormsg = $errormsg;
427fa27c 497 $client->log_out();
7eb50b32 498 redirect(new moodle_url('/login/index.php'));
1dca8d1a 499 }
1dca8d1a 500
6cee96c8
DW
501 if (!empty($CFG->authpreventaccountcreation)) {
502 // Trigger login failed event.
503 $failurereason = AUTH_LOGIN_UNAUTHORISED;
504 $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
505 'reason' => $failurereason]]);
506 $event->trigger();
507 // The username does not exist and settings prevent creating new accounts.
508 $reason = get_string('loginerror_cannotcreateaccounts', 'auth_oauth2');
509 $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason);
510 $SESSION->loginerrormsg = $errormsg;
427fa27c 511 $client->log_out();
7eb50b32 512 redirect(new moodle_url('/login/index.php'));
6cee96c8
DW
513 }
514
859e2033
DW
515 if ($issuer->get('requireconfirmation')) {
516 $PAGE->set_url('/auth/oauth2/confirm-account.php');
517 $PAGE->set_context(context_system::instance());
28b592d5 518
859e2033
DW
519 // Create a new (unconfirmed account) and send an email to confirm it.
520 $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer);
60237253 521
859e2033
DW
522 $this->update_picture($user);
523 $emailconfirm = get_string('emailconfirm');
524 $message = get_string('emailconfirmsent', '', $userinfo['email']);
525 $this->print_confirm_required($emailconfirm, $message);
526 exit();
527 } else {
528 // Create a new confirmed account.
529 $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer);
530 $userinfo = get_complete_user_data('id', $newuser->id);
531
532 // No redirect, we will complete this login.
533 }
28b592d5 534 }
c21a66e4 535 }
60237253 536
f3315133
DW
537 // We used to call authenticate_user - but that won't work if the current user has a different default authentication
538 // method. Since we now ALWAYS link a login - if we get to here we can directly allow the user in.
539 $user = (object) $userinfo;
540 complete_user_login($user);
541 $this->update_picture($user);
542 redirect($redirecturl);
60237253
DW
543 }
544}