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