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