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 * Class for loading/storing oauth2 linked logins from the DB. 19 * 20 * @package auth_oauth2 21 * @copyright 2017 Damyon Wiese 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24namespace auth_oauth2; 25 26use context_user; 27use stdClass; 28use moodle_exception; 29use moodle_url; 30 31defined('MOODLE_INTERNAL') || die(); 32 33/** 34 * Static list of api methods for auth oauth2 configuration. 35 * 36 * @package auth_oauth2 37 * @copyright 2017 Damyon Wiese 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40class api { 41 42 /** 43 * Remove all linked logins that are using issuers that have been deleted. 44 * 45 * @param int $issuerid The issuer id of the issuer to check, or false to check all (defaults to all) 46 * @return boolean 47 */ 48 public static function clean_orphaned_linked_logins($issuerid = false) { 49 return linked_login::delete_orphaned($issuerid); 50 } 51 52 /** 53 * List linked logins 54 * 55 * Requires auth/oauth2:managelinkedlogins capability at the user context. 56 * 57 * @param int $userid (defaults to $USER->id) 58 * @return boolean 59 */ 60 public static function get_linked_logins($userid = false) { 61 global $USER; 62 63 if ($userid === false) { 64 $userid = $USER->id; 65 } 66 67 if (\core\session\manager::is_loggedinas()) { 68 throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); 69 } 70 71 $context = context_user::instance($userid); 72 require_capability('auth/oauth2:managelinkedlogins', $context); 73 74 return linked_login::get_records(['userid' => $userid, 'confirmtoken' => '']); 75 } 76 77 /** 78 * See if there is a match for this username and issuer in the linked_login table. 79 * 80 * @param string $username as returned from an oauth client. 81 * @param \core\oauth2\issuer $issuer 82 * @return stdClass User record if found. 83 */ 84 public static function match_username_to_user($username, $issuer) { 85 $params = [ 86 'issuerid' => $issuer->get('id'), 87 'username' => $username 88 ]; 89 $result = linked_login::get_record($params); 90 91 if ($result) { 92 $user = \core_user::get_user($result->get('userid')); 93 if (!empty($user) && !$user->deleted) { 94 return $result; 95 } 96 } 97 return false; 98 } 99 100 /** 101 * Link a login to this account. 102 * 103 * Requires auth/oauth2:managelinkedlogins capability at the user context. 104 * 105 * @param array $userinfo as returned from an oauth client. 106 * @param \core\oauth2\issuer $issuer 107 * @param int $userid (defaults to $USER->id) 108 * @param bool $skippermissions During signup we need to set this before the user is setup for capability checks. 109 * @return bool 110 */ 111 public static function link_login($userinfo, $issuer, $userid = false, $skippermissions = false) { 112 global $USER; 113 114 if ($userid === false) { 115 $userid = $USER->id; 116 } 117 118 if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { 119 throw new moodle_exception('alreadylinked', 'auth_oauth2'); 120 } 121 122 if (\core\session\manager::is_loggedinas()) { 123 throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); 124 } 125 126 $context = context_user::instance($userid); 127 if (!$skippermissions) { 128 require_capability('auth/oauth2:managelinkedlogins', $context); 129 } 130 131 $record = new stdClass(); 132 $record->issuerid = $issuer->get('id'); 133 $record->username = $userinfo['username']; 134 $record->userid = $userid; 135 $existing = linked_login::get_record((array)$record); 136 if ($existing) { 137 $existing->set('confirmtoken', ''); 138 $existing->update(); 139 return $existing; 140 } 141 $record->email = $userinfo['email']; 142 $record->confirmtoken = ''; 143 $record->confirmtokenexpires = 0; 144 $linkedlogin = new linked_login(0, $record); 145 return $linkedlogin->create(); 146 } 147 148 /** 149 * Send an email with a link to confirm linking this account. 150 * 151 * @param array $userinfo as returned from an oauth client. 152 * @param \core\oauth2\issuer $issuer 153 * @param int $userid (defaults to $USER->id) 154 * @return bool 155 */ 156 public static function send_confirm_link_login_email($userinfo, $issuer, $userid) { 157 $record = new stdClass(); 158 $record->issuerid = $issuer->get('id'); 159 $record->username = $userinfo['username']; 160 $record->userid = $userid; 161 if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { 162 throw new moodle_exception('alreadylinked', 'auth_oauth2'); 163 } 164 $record->email = $userinfo['email']; 165 $record->confirmtoken = random_string(32); 166 $expires = new \DateTime('NOW'); 167 $expires->add(new \DateInterval('PT30M')); 168 $record->confirmtokenexpires = $expires->getTimestamp(); 169 170 $linkedlogin = new linked_login(0, $record); 171 $linkedlogin->create(); 172 173 // Construct the email. 174 $site = get_site(); 175 $supportuser = \core_user::get_support_user(); 176 $user = get_complete_user_data('id', $userid); 177 178 $data = new stdClass(); 179 $data->fullname = fullname($user); 180 $data->sitename = format_string($site->fullname); 181 $data->admin = generate_email_signoff(); 182 $data->issuername = format_string($issuer->get('name')); 183 $data->linkedemail = format_string($linkedlogin->get('email')); 184 185 $subject = get_string('confirmlinkedloginemailsubject', 'auth_oauth2', format_string($site->fullname)); 186 187 $params = [ 188 'token' => $linkedlogin->get('confirmtoken'), 189 'userid' => $userid, 190 'username' => $userinfo['username'], 191 'issuerid' => $issuer->get('id'), 192 ]; 193 $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params); 194 195 $data->link = $confirmationurl->out(false); 196 $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data); 197 198 $data->link = $confirmationurl->out(); 199 $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true); 200 201 $user->mailformat = 1; // Always send HTML version as well. 202 203 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 204 return email_to_user($user, $supportuser, $subject, $message, $messagehtml); 205 } 206 207 /** 208 * Look for a waiting confirmation token, and if we find a match - confirm it. 209 * 210 * @param int $userid 211 * @param string $username 212 * @param int $issuerid 213 * @param string $token 214 * @return boolean True if we linked. 215 */ 216 public static function confirm_link_login($userid, $username, $issuerid, $token) { 217 if (empty($token) || empty($userid) || empty($issuerid) || empty($username)) { 218 return false; 219 } 220 $params = [ 221 'userid' => $userid, 222 'username' => $username, 223 'issuerid' => $issuerid, 224 'confirmtoken' => $token, 225 ]; 226 227 $login = linked_login::get_record($params); 228 if (empty($login)) { 229 return false; 230 } 231 $expires = $login->get('confirmtokenexpires'); 232 if (time() > $expires) { 233 $login->delete(); 234 return; 235 } 236 $login->set('confirmtokenexpires', 0); 237 $login->set('confirmtoken', ''); 238 $login->update(); 239 return true; 240 } 241 242 /** 243 * Create an account with a linked login that is already confirmed. 244 * 245 * @param array $userinfo as returned from an oauth client. 246 * @param \core\oauth2\issuer $issuer 247 * @return bool 248 */ 249 public static function create_new_confirmed_account($userinfo, $issuer) { 250 global $CFG, $DB; 251 require_once($CFG->dirroot.'/user/profile/lib.php'); 252 require_once($CFG->dirroot.'/user/lib.php'); 253 254 $user = new stdClass(); 255 $user->username = $userinfo['username']; 256 $user->email = $userinfo['email']; 257 $user->auth = 'oauth2'; 258 $user->mnethostid = $CFG->mnet_localhost_id; 259 $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : ''; 260 $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : ''; 261 $user->url = isset($userinfo['url']) ? $userinfo['url'] : ''; 262 $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : ''; 263 $user->secret = random_string(15); 264 265 $user->password = ''; 266 // This user is confirmed. 267 $user->confirmed = 1; 268 269 $user->id = user_create_user($user, false, true); 270 271 // The linked account is pre-confirmed. 272 $record = new stdClass(); 273 $record->issuerid = $issuer->get('id'); 274 $record->username = $userinfo['username']; 275 $record->userid = $user->id; 276 $record->email = $userinfo['email']; 277 $record->confirmtoken = ''; 278 $record->confirmtokenexpires = 0; 279 280 $linkedlogin = new linked_login(0, $record); 281 $linkedlogin->create(); 282 283 return $user; 284 } 285 286 /** 287 * Send an email with a link to confirm creating this account. 288 * 289 * @param array $userinfo as returned from an oauth client. 290 * @param \core\oauth2\issuer $issuer 291 * @param int $userid (defaults to $USER->id) 292 * @return bool 293 */ 294 public static function send_confirm_account_email($userinfo, $issuer) { 295 global $CFG, $DB; 296 require_once($CFG->dirroot.'/user/profile/lib.php'); 297 require_once($CFG->dirroot.'/user/lib.php'); 298 299 if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { 300 throw new moodle_exception('alreadylinked', 'auth_oauth2'); 301 } 302 303 $user = new stdClass(); 304 $user->username = $userinfo['username']; 305 $user->email = $userinfo['email']; 306 $user->auth = 'oauth2'; 307 $user->mnethostid = $CFG->mnet_localhost_id; 308 $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : ''; 309 $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : ''; 310 $user->url = isset($userinfo['url']) ? $userinfo['url'] : ''; 311 $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : ''; 312 $user->secret = random_string(15); 313 314 $user->password = ''; 315 // This user is not confirmed. 316 $user->confirmed = 0; 317 318 $user->id = user_create_user($user, false, true); 319 320 // The linked account is pre-confirmed. 321 $record = new stdClass(); 322 $record->issuerid = $issuer->get('id'); 323 $record->username = $userinfo['username']; 324 $record->userid = $user->id; 325 $record->email = $userinfo['email']; 326 $record->confirmtoken = ''; 327 $record->confirmtokenexpires = 0; 328 329 $linkedlogin = new linked_login(0, $record); 330 $linkedlogin->create(); 331 332 // Construct the email. 333 $site = get_site(); 334 $supportuser = \core_user::get_support_user(); 335 $user = get_complete_user_data('id', $user->id); 336 337 $data = new stdClass(); 338 $data->fullname = fullname($user); 339 $data->sitename = format_string($site->fullname); 340 $data->admin = generate_email_signoff(); 341 342 $subject = get_string('confirmaccountemailsubject', 'auth_oauth2', format_string($site->fullname)); 343 344 $params = [ 345 'token' => $user->secret, 346 'username' => $userinfo['username'] 347 ]; 348 $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params); 349 350 $data->link = $confirmationurl->out(false); 351 $message = get_string('confirmaccountemail', 'auth_oauth2', $data); 352 353 $data->link = $confirmationurl->out(); 354 $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true); 355 356 $user->mailformat = 1; // Always send HTML version as well. 357 358 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 359 email_to_user($user, $supportuser, $subject, $message, $messagehtml); 360 return $user; 361 } 362 363 /** 364 * Delete linked login 365 * 366 * Requires auth/oauth2:managelinkedlogins capability at the user context. 367 * 368 * @param int $linkedloginid 369 * @return boolean 370 */ 371 public static function delete_linked_login($linkedloginid) { 372 $login = new linked_login($linkedloginid); 373 $userid = $login->get('userid'); 374 375 if (\core\session\manager::is_loggedinas()) { 376 throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); 377 } 378 379 $context = context_user::instance($userid); 380 require_capability('auth/oauth2:managelinkedlogins', $context); 381 382 $login->delete(); 383 } 384 385 /** 386 * Delete linked logins for a user. 387 * 388 * @param \core\event\user_deleted $event 389 * @return boolean 390 */ 391 public static function user_deleted(\core\event\user_deleted $event) { 392 global $DB; 393 394 $userid = $event->objectid; 395 396 return $DB->delete_records(linked_login::TABLE, ['userid' => $userid]); 397 } 398 399 /** 400 * Is the plugin enabled. 401 * 402 * @return bool 403 */ 404 public static function is_enabled() { 405 return is_enabled_auth('oauth2'); 406 } 407} 408