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->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : ''; 262 $user->secret = random_string(15); 263 264 $user->password = ''; 265 // This user is confirmed. 266 $user->confirmed = 1; 267 268 $user->id = user_create_user($user, false, true); 269 270 // The linked account is pre-confirmed. 271 $record = new stdClass(); 272 $record->issuerid = $issuer->get('id'); 273 $record->username = $userinfo['username']; 274 $record->userid = $user->id; 275 $record->email = $userinfo['email']; 276 $record->confirmtoken = ''; 277 $record->confirmtokenexpires = 0; 278 279 $linkedlogin = new linked_login(0, $record); 280 $linkedlogin->create(); 281 282 return $user; 283 } 284 285 /** 286 * Send an email with a link to confirm creating this account. 287 * 288 * @param array $userinfo as returned from an oauth client. 289 * @param \core\oauth2\issuer $issuer 290 * @param int $userid (defaults to $USER->id) 291 * @return bool 292 */ 293 public static function send_confirm_account_email($userinfo, $issuer) { 294 global $CFG, $DB; 295 require_once($CFG->dirroot.'/user/profile/lib.php'); 296 require_once($CFG->dirroot.'/user/lib.php'); 297 298 if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { 299 throw new moodle_exception('alreadylinked', 'auth_oauth2'); 300 } 301 302 $user = new stdClass(); 303 $user->username = $userinfo['username']; 304 $user->email = $userinfo['email']; 305 $user->auth = 'oauth2'; 306 $user->mnethostid = $CFG->mnet_localhost_id; 307 $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : ''; 308 $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : ''; 309 $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : ''; 310 $user->secret = random_string(15); 311 312 $user->password = ''; 313 // This user is not confirmed. 314 $user->confirmed = 0; 315 316 $user->id = user_create_user($user, false, true); 317 318 // The linked account is pre-confirmed. 319 $record = new stdClass(); 320 $record->issuerid = $issuer->get('id'); 321 $record->username = $userinfo['username']; 322 $record->userid = $user->id; 323 $record->email = $userinfo['email']; 324 $record->confirmtoken = ''; 325 $record->confirmtokenexpires = 0; 326 327 $linkedlogin = new linked_login(0, $record); 328 $linkedlogin->create(); 329 330 // Construct the email. 331 $site = get_site(); 332 $supportuser = \core_user::get_support_user(); 333 $user = get_complete_user_data('id', $user->id); 334 335 $data = new stdClass(); 336 $data->fullname = fullname($user); 337 $data->sitename = format_string($site->fullname); 338 $data->admin = generate_email_signoff(); 339 340 $subject = get_string('confirmaccountemailsubject', 'auth_oauth2', format_string($site->fullname)); 341 342 $params = [ 343 'token' => $user->secret, 344 'username' => $userinfo['username'] 345 ]; 346 $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params); 347 348 $data->link = $confirmationurl->out(false); 349 $message = get_string('confirmaccountemail', 'auth_oauth2', $data); 350 351 $data->link = $confirmationurl->out(); 352 $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true); 353 354 $user->mailformat = 1; // Always send HTML version as well. 355 356 // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. 357 email_to_user($user, $supportuser, $subject, $message, $messagehtml); 358 return $user; 359 } 360 361 /** 362 * Delete linked login 363 * 364 * Requires auth/oauth2:managelinkedlogins capability at the user context. 365 * 366 * @param int $linkedloginid 367 * @return boolean 368 */ 369 public static function delete_linked_login($linkedloginid) { 370 $login = new linked_login($linkedloginid); 371 $userid = $login->get('userid'); 372 373 if (\core\session\manager::is_loggedinas()) { 374 throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); 375 } 376 377 $context = context_user::instance($userid); 378 require_capability('auth/oauth2:managelinkedlogins', $context); 379 380 $login->delete(); 381 } 382 383 /** 384 * Delete linked logins for a user. 385 * 386 * @param \core\event\user_deleted $event 387 * @return boolean 388 */ 389 public static function user_deleted(\core\event\user_deleted $event) { 390 global $DB; 391 392 $userid = $event->objectid; 393 394 return $DB->delete_records(linked_login::TABLE, ['userid' => $userid]); 395 } 396 397 /** 398 * Is the plugin enabled. 399 * 400 * @return bool 401 */ 402 public static function is_enabled() { 403 return is_enabled_auth('oauth2'); 404 } 405} 406