/auth/oauth2/classes/api.php

https://github.com/andyjdavis/moodle · PHP · 405 lines · 226 code · 60 blank · 119 comment · 20 complexity · 7c8febeed74dd20f7da8ed3ba97579c4 MD5 · raw file

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