Commit | Line | Data |
---|---|---|
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 | ||
25 | namespace auth_oauth2; | |
26 | ||
27 | defined('MOODLE_INTERNAL') || die(); | |
28 | ||
29 | use pix_icon; | |
30 | use moodle_url; | |
31 | use core_text; | |
28b592d5 | 32 | use context_system; |
60237253 DW |
33 | use stdClass; |
34 | use core\oauth2\issuer; | |
35 | use core\oauth2\client; | |
36 | ||
37 | require_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 | */ | |
46 | class 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 | } | |
250 | $picture = $this->get_static_user_picture(); | |
251 | if (empty($picture)) { | |
252 | return false; | |
253 | } | |
254 | ||
255 | $context = \context_user::instance($userid, MUST_EXIST); | |
256 | $fs->delete_area_files($context->id, 'user', 'newicon'); | |
257 | ||
258 | $filerecord = array( | |
259 | 'contextid' => $context->id, | |
260 | 'component' => 'user', | |
261 | 'filearea' => 'newicon', | |
262 | 'itemid' => 0, | |
263 | 'filepath' => '/', | |
264 | 'filename' => 'image' | |
265 | ); | |
266 | ||
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 | } | |
272 | ||
273 | $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); | |
274 | ||
275 | // There should only be one. | |
276 | $iconfile = reset($iconfile); | |
277 | ||
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 | } | |
283 | ||
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 | } | |
298 | ||
72fd103a | 299 | /** |
28b592d5 DW |
300 | * Confirm the new user as registered. |
301 | * | |
302 | * @param string $username | |
303 | * @param string $confirmsecret | |
72fd103a | 304 | */ |
13b449f4 | 305 | public function user_confirm($username, $confirmsecret) { |
28b592d5 DW |
306 | global $DB; |
307 | $user = get_complete_user_data('username', $username); | |
308 | ||
309 | if (!empty($user)) { | |
310 | if ($user->auth != $this->authtype) { | |
311 | return AUTH_CONFIRM_ERROR; | |
312 | ||
313 | } else if ($user->secret == $confirmsecret && $user->confirmed) { | |
314 | return AUTH_CONFIRM_ALREADY; | |
315 | ||
13b449f4 DW |
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)); | |
28b592d5 DW |
318 | return AUTH_CONFIRM_OK; |
319 | } | |
320 | } else { | |
321 | return AUTH_CONFIRM_ERROR; | |
1dca8d1a | 322 | } |
28b592d5 DW |
323 | } |
324 | ||
325 | /** | |
326 | * Print a page showing that a confirm email was sent with instructions. | |
327 | * | |
13b449f4 DW |
328 | * @param string $title |
329 | * @param string $message | |
28b592d5 DW |
330 | */ |
331 | public function print_confirm_required($title, $message) { | |
332 | global $PAGE, $OUTPUT, $CFG; | |
333 | ||
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"); | |
1dca8d1a DW |
339 | } |
340 | ||
60237253 DW |
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) { | |
28b592d5 | 348 | global $CFG, $SESSION, $PAGE; |
60237253 DW |
349 | |
350 | $userinfo = $client->get_userinfo(); | |
351 | ||
352 | if (!$userinfo) { | |
6cee96c8 DW |
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(); | |
358 | ||
7f158660 | 359 | $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2'); |
60237253 | 360 | $SESSION->loginerrormsg = $errormsg; |
427fa27c | 361 | $client->log_out(); |
60237253 | 362 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
8445556b | 363 | } |
28b592d5 | 364 | if (empty($userinfo['username']) || empty($userinfo['email'])) { |
6cee96c8 DW |
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(); | |
370 | ||
7f158660 | 371 | $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2'); |
8445556b | 372 | $SESSION->loginerrormsg = $errormsg; |
427fa27c | 373 | $client->log_out(); |
8445556b | 374 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
60237253 DW |
375 | } |
376 | ||
377 | $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); | |
427fa27c | 378 | $oauthemail = $userinfo['email']; |
60237253 | 379 | |
28b592d5 | 380 | // Once we get here we have the user info from oauth. |
1dca8d1a | 381 | $userwasmapped = false; |
1dca8d1a | 382 | |
28b592d5 DW |
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 | } | |
388 | ||
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 | } | |
395 | ||
396 | // First we try and find a defined mapping. | |
397 | $linkedlogin = api::match_username_to_user($userinfo['username'], $client->get_issuer()); | |
398 | ||
399 | if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) { | |
400 | $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid')); | |
401 | ||
402 | if ($mappeduser && $mappeduser->confirmed) { | |
1dca8d1a DW |
403 | $userinfo = (array) $mappeduser; |
404 | $userwasmapped = true; | |
28b592d5 | 405 | } else { |
6cee96c8 DW |
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(); | |
411 | ||
28b592d5 DW |
412 | $errormsg = get_string('confirmationpending', 'auth_oauth2'); |
413 | $SESSION->loginerrormsg = $errormsg; | |
427fa27c | 414 | $client->log_out(); |
28b592d5 | 415 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
1dca8d1a | 416 | } |
28b592d5 | 417 | } else if (!empty($linkedlogin)) { |
6cee96c8 DW |
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(); | |
423 | ||
28b592d5 DW |
424 | $errormsg = get_string('confirmationpending', 'auth_oauth2'); |
425 | $SESSION->loginerrormsg = $errormsg; | |
427fa27c | 426 | $client->log_out(); |
28b592d5 DW |
427 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
428 | } | |
427fa27c | 429 | |
28b592d5 | 430 | $issuer = $client->get_issuer(); |
427fa27c | 431 | if (!$issuer->is_valid_login_domain($oauthemail)) { |
6cee96c8 DW |
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(); | |
437 | ||
7f158660 | 438 | $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2')); |
28b592d5 | 439 | $SESSION->loginerrormsg = $errormsg; |
427fa27c | 440 | $client->log_out(); |
28b592d5 | 441 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
60237253 DW |
442 | } |
443 | ||
1dca8d1a | 444 | if (!$userwasmapped) { |
28b592d5 DW |
445 | // No defined mapping - we need to see if there is an existing account with the same email. |
446 | ||
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()); | |
451 | ||
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(); | |
458 | ||
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) { | |
6cee96c8 DW |
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(); | |
28b592d5 DW |
469 | |
470 | // The username exists but the emails don't match. Refuse to continue. | |
471 | $errormsg = get_string('accountexists', 'auth_oauth2'); | |
472 | $SESSION->loginerrormsg = $errormsg; | |
427fa27c | 473 | $client->log_out(); |
28b592d5 DW |
474 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
475 | } | |
1dca8d1a | 476 | |
28b592d5 | 477 | if (email_is_not_allowed($userinfo['email'])) { |
6cee96c8 DW |
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(); | |
28b592d5 | 483 | // The username exists but the emails don't match. Refuse to continue. |
7f158660 DW |
484 | $reason = get_string('loginerror_invaliddomain', 'auth_oauth2'); |
485 | $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); | |
28b592d5 | 486 | $SESSION->loginerrormsg = $errormsg; |
427fa27c | 487 | $client->log_out(); |
28b592d5 | 488 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
1dca8d1a | 489 | } |
1dca8d1a | 490 | |
6cee96c8 DW |
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; | |
427fa27c | 501 | $client->log_out(); |
6cee96c8 DW |
502 | redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); |
503 | } | |
504 | ||
28b592d5 DW |
505 | $PAGE->set_url('/auth/oauth2/confirm-account.php'); |
506 | $PAGE->set_context(context_system::instance()); | |
507 | ||
508 | // Create a new (unconfirmed account) and send an email to confirm it. | |
509 | $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer); | |
60237253 | 510 | |
28b592d5 DW |
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(); | |
28b592d5 | 516 | } |
c21a66e4 | 517 | } |
60237253 | 518 | |
f3315133 DW |
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); | |
60237253 DW |
525 | } |
526 | } | |
527 | ||
528 |